Bits of Learning

Learning sometimes happens in big jumps, but mostly in little tiny steps. I share my baby steps of learning here, mostly on topics around programming, programming languages, software engineering, and computing in general. But occasionally, even on other disciplines of engineering or even science. I mostly learn through examples and doing. And this place is a logbook of my experiences in learning something. You may find several things interesting here: little cute snippets of (hopefully useful) code, a bit of backing theory, and a lot of gyan on how learning can be so much fun.

Wednesday, December 13, 2023

Test Driven Development -- An Example

This post is written as a model answer to an exam question. As I have clarified earlier, this is way too long to be written in the exam. But, it suffices as an example.

The main philosophy of test-driven development is:
  • Use test cases as specifications.
  • Test cases should be written preferably before the part of the implementation they test. Hence, a newly introduced test would typically fail. Then the code should be modified to make it pass, possibly followed by some refactoring to improve the design.
  • We stop coding as soon as we get all test cases to pass.
  • If we are thinking of a new feature, or cases within the new feature, we should first write a test case corresponding to that.
These tests become part of the project assets, and can be used later at any point in time. TDD is aligned with agile methodology as it focuses on creating work code as opposed to separate documentation.

First set up the testing environment. For simplicity and self-sufficiency, I have created my own TestResult class.
#include <iostream>

using namespace std;

class TestResult {
public:
	const bool result;
	const string message;

	TestResult(bool r, string m) : result(r), message(m) {}
};

The algorithm for palindrome detection that I want to use is the following:

match S with:

case "" --> raise exception

case "x" --> true

case "xy" --> x = y

case "xS'y" --> x = y /\ pal(S')

This is not the most efficient implementation. But that is not the point here. We intend this to be an illustration of the test-driven methodology.

Going by the above algorithm, the first case we would like to test is that if an empty string is given, the function should throw an exception. The test case for checking this is as follows:

TestResult t1() {
	try {
		isPalindrome("");
		TestResult r(false, "Exception not thrown");
		return r;
	}
	catch (string e) {
		TestResult r(true, e);
		return r;
	}
}

The main function or the driver for now is as follows:

int main() {
	TestResult r1 = t1();
	cout << r1.result << ", " << r1.message << endl;
	return 0;
}

The first cut implementation of the isPalindrome function is as follows:

bool isPalindrome(string s) {
	return true;
}

As you can see, it's designed to fail t1. And when we run it, it indeed does:

0, Exception not thrown

Next we modify the function to make t1 pass.

bool isPalindrome(string s) {
	if(s == "") {
		throw string("Empty string provided");
	}
	return true;
}

t1 passes:

1, Empty string provided

Now, we wish to handle the next case.

case "x" --> true

TestResult t2() {
	try {
		bool ans = isPalindrome("a");
		TestResult r(ans == true, "");
		return r;
	}
	catch (string e) {
		TestResult r(false, "Exception: " + e);
		return r;
	}
}

This test passes without any need of change to the code.

So, we move over to the next case:

case "xy" --> x = y

TestResult t3() {
	try {
		bool ans = isPalindrome("aa");
		TestResult r(ans == true, "Correct!");
		return r;
	}
	catch (string e) {
		TestResult r(false, "Exception: " + e);
		return r;
	}
}

This passes:

1, Empty string provided
1, 
1, Correct!

But we need to test this case more, for the subcase when we are expecting a negative answer:

TestResult t4() {
	try {
		bool ans = isPalindrome("ab");
		bool result = ans == false;
		string message = result ? "Correct" : "Wrong answer";
		TestResult r(ans == false, message);
		return r;
	}
	catch (string e) {
		TestResult r(false, "Exception: " + e);
		return r;
	}
}

As expected, this test case fails:

1, Empty string provided
1, 
1, Correct!
0, Wrong answer

The palindrome function needs to be modified appropriately to make this test case pass:

bool isPalindrome(string s) {
	if(s == "") {
		throw string("Empty string provided");
	}
	if(s.size() == 1) {
	  return true;
	}
	if(s.size() == 2) {
		return s[0] == s[1];
	}
}

Now, t4 passes:

1, Empty string provided
1, 
1, Correct!
1, Correct

Now that we have tested the third case to our satisfaction, we move over to the final case:

case "xS'y" --> x = y and pal(S')

We first add a test case for testing the final case:

TestResult t5() {
	try {
		bool ans = isPalindrome("aba");
		bool result = ans == true;
		string message = result ? "Correct" : "Wrong answer";
		TestResult r(result, message);
		return r;
	}
	catch (string e) {
		TestResult r(false, "Exception: " + e);
		return r;
	}
}

t5 passes without any need of code change.

1, Empty string provided
1, 
1, Correct!
1, Correct
1, Correct

Let's add t6 to check the negative case:

TestResult t6() {
	try {
		bool ans = isPalindrome("abb");
		bool result = ans == false;
		string message = result ? "Correct" : "Wrong answer";
		TestResult r(result, message);
		return r;
	}
	catch (string e) {
		TestResult r(false, "Exception: " + e);
		return r;
	}
}

This fails:

1, Empty string provided
1, 
1, Correct!
1, Correct
1, Correct
0, Wrong answer

This requires us to implement another function, say 'sub', which gives us a copy of the passed string with the first and the last character omitted. Formally:

"xS'y"  --> raise exception if S' = ""

--> S' if S' != ""

This should be used on strings with length greater than or equal to 2. We make some code changes to make place for the sub function. We begin with a dummy implementation of sub as follows:

string sub(string s) {
  return "Hello";
}

bool isPalindrome(string s) {
	if(s == "") {
		throw string("Empty string provided");
	}
	if(s.size() == 1) {
	  return true;
	}
	if(s.size() == 2) {
		return s[0] == s[1];
	}
	
	return s[0] == s[s.size() - 1] && isPalindrome(sub(s));
}

The above causes t6 to pass, but breaks t5.

1, Empty string provided
1, 
1, Correct!
1, Correct
0, Wrong answer
1, Correct

We understand that this is because of the erroneous implementation of sub function. So, we turn our attention to sub.

We start with a test case to check its first case:

"xS'y"  --> raise exception if S' = ""

TestResult t7() {
	try {
		string ans = sub("a");
		TestResult r(false, "Exception not thrown");
		return r;
	}
	catch (string e) {
		TestResult r(true, e);
		return r;
	}
}

t7 fails. And we make the requisite code modification to get it to pass.

string sub(string s) {
	if(s.size() < 2) {
		throw string("Insufficient size.");
	}
	return "Hello";
}

For sure, t7 passes:

1, Empty string provided
1, 
1, Correct!
1, Correct
0, Wrong answer
1, Correct
1, Insufficient size.

But t5 is still failing. Clearly, all is not well with sub. Anyway, let's proceed to handle the other case in sub:

--> S' if S' != ""

We jump any ceremony and make the following change to sub:

string sub(string s) {
	if(s.size() < 2) {
		throw string("Insufficient size.");
	}
	string new_s = "";
	for(unsigned int i = 1; i < s.size() - 1; i++) {
		new_s += s[i];
	}
	return new_s;
}

t5 passes:

1, Empty string provided
1, 
1, Correct!
1, Correct
1, Correct
1, Correct
1, Insufficient size.

This has presumably completed our development of the code. However, we need to run it through some more tests before we can call it complete.

TestResult t8() {
try { bool ans = isPalindrome("aba"); bool result = ans == true; string message = result ? "Correct" : "Wrong answer"; TestResult r(result, message); return r; } catch (string e) { TestResult r(false, "Exception: " + e); return r; } } TestResult t9() { try { bool ans = isPalindrome("abba"); bool result = ans == true; string message = result ? "Correct" : "Wrong answer"; TestResult r(result, message); return r; } catch (string e) { TestResult r(false, "Exception: " + e); return r; } } TestResult t10() { try { bool ans = isPalindrome("aaba"); bool result = ans == false; string message = result ? "Correct" : "Wrong answer"; TestResult r(result, message); return r; } catch (string e) { TestResult r(false, "Exception: " + e); return r; } }

To our pleasure, all these new test cases pass in one shot!

1, Empty string provided
1, 
1, Correct!
1, Correct
1, Correct
1, Correct
1, Insufficient size.
1, Correct
1, Correct
1, Correct

This completes the test driven development of the isPalindrome function.

Wednesday, March 03, 2021

Automating Your Build with Makefiles

I know I am getting into a well-known topic. What more, make is a pretty old technology. We have so many other cooler build automation technologies out there. IDEs like Eclipse and Visual Studio make it unnecessary to write your old build system in the first place.

After the above negative marketing, let me tell you why then I am writing this post: because understanding how dependencies work between the various modules of your software is fundamentally connected to your design thinking. In fact, understanding how make's algorithm works at a high level is worth the effort. I present a prototype implementation of the same below, all of ~30 lines of OCaml code.

A Running Example

(Adapted from a illustrative project by my student Anirudh C.)


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
(* arith.ml -- begin *)
let inc x = x + 1
let dec x = x - 1

let add x y =
  let rec loop x y =
    if y = 0 then x else loop (inc x) (dec y)
  in loop x y

let sub x y =
  let rec loop x y =
    if y = 0 then x else loop (dec x) (dec y)
  in loop x y
(* arith.ml -- end *)  
  
(* circle.ml -- begin*)
let pi = 3.141
let area r = pi *. r *. r
let circumference r = 2. *. pi *. r
(* circle.ml - end *)

(* main.ml -- begin *)
let main () =
  print_endline (string_of_int (Arith.add 1 2));
  print_endline (string_of_int (Arith.sub 3 2));

  print_endline (string_of_float (Circle.area 2.))

let _ = main ()
(* main.ml -- end *)

In the code shown above, we have three modules, arith (arith.ml, arith.mli), circle (circle.ml, circle.mli) and main (main.ml, main.mli). main uses both arith and circle which are self contained modules. The OCaml implementation of the above idea, therefore, has the following additional files:

arith.cmi: The object code for the interface of arith module

arith.cmo: The object code for the implementation of arith module

circle.cmi: The object code for the interface of circle module

circle.cmo: The object code for the implementation of circle module

main.cmi: The object code for the interface of main module

main.cmo: The object code for the implementation of main module

app: The final executable.

 Here are the listings of the interface files:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
(* arith.mli -- begin *)
val add : int -> int -> int
val sub : int -> int -> int
(* arith.mli -- end *)  
  
(* circle.mli -- begin*)
val area : float -> float
val circumference : float -> float
(* circle.mli - end *)

(* main.mli -- begin *)
val main: unit -> unit
(* main.mli -- end *)

 

The dependencies between the various files is shown by the following directed acyclic graph:


As you can see, the dependencies look a bit complicated, if not a lot. Part of why it's this way has to do with the particular programming language that you are using. For example, OCaml has this idea of object code being generated from the interface files by compiling the .mli file. However, the most important aspect of this structure is agnostic to the programming language. That aspect of the dependencies is its directed acyclic nature. In the dependency graph above, you will find no cycles or loops.

What impact do the above dependency relations have on build automation process. Here is the exhaustive sequence of build steps:

1
2
3
4
5
6
7
ocamlc -c arith.mli                         # builds arith.cmi
ocamlc -c arith.ml                          # builds arith.cmo
ocamlc -c circle.mli                        # builds circle.cmi
ocamlc -c circle.ml                         # builds circle.cmo
ocamlc -c main.mli                          # builds main.cmi
ocamlc -c main.ml                           # builds main.cmo
ocamlc -o app arith.cmo circle.cmo main.cmo # builds app


The first influence is the following: The dependee must always be compiled after all its dependencies have been compiled. The sequence in which the above steps are written respects this order.

The second one is an opportunity of optimisation: When you are building the application, you compile only that part of the application which is depends on any part which has been modified since the last build. For example, if we make a modification in the circle.ml file, which parts of the build process need to be repeated? Looking at the dependency graph we can say that its circle.cmo, and app, because these are only two nodes in the graph from which you can reach circle.ml by following the directed edges of the dependence graph.

 The make Algorithm

The make algorithm allows us to implement the above optimisation. We specify the dependency graph in a makefile. Here's the Makefile for the above application:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
app : arith.cmo circle.cmo main.cmo
	ocamlc -o app arith.cmo circle.cmo main.cmo

arith.cmo: arith.ml arith.cmi
	ocamlc -c arith.ml

circle.cmo: circle.ml circle.cmi
	ocamlc -c circle.ml

arith.cmi : arith.mli
	ocamlc -c arith.mli

circle.cmi : circle.mli
	ocamlc -c circle.mli

main.cmo : main.ml main.cmi arith.cmi circle.cmi
	ocamlc -c main.ml

main.cmi : main.mli
	ocamlc -c main.mli



 

 

 

make algorithm takes this graph as an input, studies the modification timestamps of all the targets and does just what is necessary (and useful) in the build process of the application. What's the rule to be followed here? Whenever any of the dependencies, say d, of a dependee/target file f was modified after f was modified, f needs to be rebuilt. However, one more bit. The timestamp of f and d is checked only after this rule is applied on d. And recursively. While an untrained human mind may find this a bit mind-bending, it's really a very very simple algorithm.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
function process_node(n)
  if(n.deps != {}) then
    process_node(n') for all n' in n.deps
    maxdepts = max(timestamps(n.deps))
    if(timestamp(n) < maxdepts) then
      execute(n.action)
      update_timestamp(n, now)
    endif
  endif
end


The algorithm above does a depth first traversal starting with the target which we are building. After having processed all the dependencies, it compares the timestamp of the current target with those of all its dependencies. If any one of them is more recently modified than the target, the target is rebuilt by executing its build command.

To round up the article, here's the OCaml implementation of the above algorithm. Even though the code has a good 170 LOC, note that the core algorithm is just 33 lines (L42 to L72). Rest of it is all plumbing and testing code.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
type node = {
  name : bytes;
  dependencies : node list;
  action : bytes;
}

type timestamp = {
  n : node;
  t : int;
}

let string_of_timestamp ts =
  "\t(" ^ ts.n.name ^ ", " ^ (string_of_int ts.t) ^ ")"

let string_of_timestamps tslist =
  "[" ^
    (let strtss = List.map string_of_timestamp tslist in
    (List.fold_left (fun x y -> x ^ y ^ "\n") "" strtss)) ^
  "]" 

exception My_exception of string

let rec get_timestamp timestamps n' =
  match timestamps with
    [] -> raise (My_exception n'.name)
  | { n; t } :: tl ->
    if n = n' then t
    else (get_timestamp tl n')

let rec update_timestamp timestamps n' t' =
  match timestamps with
    [] -> []
  | { n = n; t = t } :: tl ->
    if n = n' then { n = n'; t = t'} :: tl
    else { n ; t } :: (update_timestamp tl n' t')

let rec max = function
    [] -> raise Not_found
  | [h] -> h
  | h :: t -> let maxt = max t in if h > maxt then h else maxt

let rec process_node n ts =
  let myt = get_timestamp ts n in
  if (List.length n.dependencies <> 0) then
    let depts = process_dependencies n.dependencies ts in
    let maxts = max (List.map (get_timestamp depts) n.dependencies) in
    if myt < maxts then
    begin
      print_endline ("executing " ^ n.action);
      let ts' = update_timestamp depts n (maxts + 1) in
      begin
        print_endline (string_of_timestamps ts');
        ts'
      end
    end
    else
    begin
      print_endline ("Nothing to be done for " ^ n.name);
      ts
    end
  else
    begin
      print_endline ("Nothing to be done for " ^ n.name);
      ts
    end

and process_dependencies deps ts =
  let rec loop deps ts =
    match deps with
      [] -> ts
    | d :: deps' ->
        let ts' = process_node d ts in loop deps' ts'
  in
  loop deps ts

let arith_mli = {
  name = "arith.mli";
  dependencies = [];
  action = "";
}
let arith_cmi = {
  name = "arith.cmi";
  dependencies = [arith_mli];
  action = "ocamlc -c arith.mli"
}
let arith_ml = {
  name = "arith.ml";
  dependencies = [];
  action = "";
}
let arith_cmo = {
  name = "arith.cmo";
  dependencies = [arith_ml; arith_cmi];
  action = "ocamlc -c arith.ml";
}
let circle_mli = {
  name = "circle.mli";
  dependencies = [];
  action = "";
}
let circle_cmi = {
  name = "circle.cmi";
  dependencies = [circle_mli];
  action = "ocamlc -c circle.mli"
}
let circle_ml = {
  name = "circle.ml";
  dependencies = [];
  action = "";
}
let circle_cmo = {
  name = "circle.cmo";
  dependencies = [circle_ml; circle_cmi];
  action = "ocamlc -c circle.ml";
}
let main_mli = {
  name = "main.mli";
  dependencies = [];
  action = "";
}
let main_cmi = {
  name = "main.cmi";
  dependencies = [main_mli];
  action = "ocamlc -c main.mli"
}
let main_ml = {
  name = "main.ml";
  dependencies = [];
  action = "";
}
let main_cmo = {
  name = "main.cmo";
  dependencies = [main_ml; main_cmi; circle_cmi; arith_cmi];
  action = "ocamlc -c main.ml";
}
let app = {
  name = "app";
  dependencies = [arith_cmo; circle_cmo; main_cmo];
  action = "ocamlc -o app arith.cmo circle.cmo main.cmo";
}
 
let ts = [
  {n = arith_mli;  t = 1};
  {n = arith_cmi;  t = 0};
  {n = arith_ml;   t = 0};
  {n = arith_cmo;  t = 0};
  {n = circle_mli; t = 0};
  {n = circle_cmi; t = 0};
  {n = circle_ml;  t = 0};
  {n = circle_cmo; t = 0};
  {n = main_mli;   t = 0};
  {n = main_cmi;   t = 0};
  {n = main_ml;    t = 0};
  {n = main_cmo;   t = 0};
  {n = app;        t = 0};
]

let t1 () =
  process_node app ts

let t2 () =
  (update_timestamp ts main_cmo 10) |> string_of_timestamps |> print_endline 

let t3 () =
  (get_timestamp ts arith_mli) |> string_of_int |> print_endline;
  (get_timestamp ts circle_mli) |> string_of_int |> print_endline

let _ = t2 ()
let _ = t1 ()
let _ = t3 ()


Hope you found the above useful and interesting!

Wednesday, November 18, 2020

The Fascinating Art of Making Notes

Note-taking is a regular activity anyone wanting to record his reading, thoughts, ideas, etc. does on a regular basis. It could be a student, a teacher, or any expert. Note-taking happens in a variety of situations. One of the most prevalent is the classroom, lectures or meetings. For me, I make notes mostly in the following situations:

  • Thinking, e.g. for my research
  • Reading
  • Preparing for lectures

In this article, I wish to share some of my own methods, practices and observations about making notes. My thoughts around this topic are focused around drawing figures. Most if not all of my understanding must be translated into some visual language (which broadly includes mathematical notations and program code too) to be checked off as per my personal QA standards. This mayn't be a universal method as different people have different styles of comprehending their thoughts. I am a visual thinker. No surprise, I draw lots of pictures to express my thoughts, and to test their clarity.

While we all try to represent our abstract thoughts as viewable artifacts, I am sure most of us pretty much muddle through the process without fully understanding what's going on. One objective of writing this piece is also to unpack this mental process, as I understand it. No rocket science here. But, there's always merit in sharing.

All visualisation of our thoughts has two main purposes. One is to help us think, and the other is as a communication tool.

Note-making as a Thinking Tool

When we are using writing/drawing as a tool for understanding, flexibility is important. What you write or draw should be malleable enough to move in step with your thoughts. When the artifact you are thinking about appears like a rectangle, an ellipse or a blob with some text in it, you want to go ahead and put that out on paper. It doesn't help searching for an appropriate shape or object in the shapes library of your favourite editor. Doing so has at least the following disadvantages:

  1. It comes in the way of your thoughts, slowing you down
  2. It increases the cognitive load further impending thoughts
  3. It tends to influence the way you want to show your thoughts. This may hamper the freedom with which you think.

With the current state-of-the-art, computing devices are yet to become portable and lightweight enough to be readily available everywhere.

Hence, I personally prefer handwriting/hand-drawing as the preferred tool for this. I feel that nothing can match a notebook and a pen/pencil when it comes to providing that flexibility.

Traditional -- Pen/pencil + Notebook

Handwritten characters and shapes are closest to our thoughts


Pens are more beautiful and comfortable writing devices. Output, if got right, is of course superior to that of a pencil. Pencils (coupled with erasers) are more forgiving of mistakes as you can correct them.

Advantages of Traditional:  
  1. Flexible
  2. Handy and available
  3. Inexpensive
  4. No learning required


Disadvantages of Traditional:  
  1. All manual
  2. Limited
  3. Messy
  4. Paper based



 

Digital tablets

The state-of-the-art has now come close to the point where the best of both the worlds -- digital and traditional -- are now available in digital. You can handwrite, hand-draw to your heart's content if you have a stylus enabled device like a tablet computer or a Wacom tablet like device. There are further advantages which make digital handwriting a very compelling choice for making your first handwritten notes. The most important is flexibility. When it comes to correcting mistakes, even a pencil and an eraser have their limits; while in a digital writing pad, mistakes are easily forgiven and forgotten. You can correct and tweak to an almost infinite extent, preventing the write-ups or figures from getting messy with more edits. The richness and variety of tools that you get in the digital medium can't easily be matched by traditional medium. Pen colours, thickness, shapes, backgrounds, templates ... If all these blend seamlessly with handwriting/drawing, then the flexibility of the digital medium is just irresistible.

While traditional is incomparable in its experience, correcting mistakes gets harder as you go.

 
The richness of tools, variety, flexibility of digital medium is incomparable

For me, first cut digital notes are nearly undistisguishable from their traditional counterpart


And to top it all, copy-paste often comes as a boon for re-using your earlier efforts and in maintaining a uniform look and feel. While, this part mayn't be terribly important when you are making personal notes, it's important when you have drawn something very complicated, or when you want your personal notes to evolve into fair notes for sharing with others.

 

Advantages of Digital Handwriting/drawing: 
  1. Flexible
  2. Versatile
  3. Integrates seamlessly with completely digital tools


Disadvantages of Digital Handwriting/drawing: 
  1. Expensive
  2. May involve learning
  3. Not as portable and available as traditional medium

Note-making as a Communication Tool

Once your thoughts are clear enough and the visualisation thereof have attained a respectable look, you may or mayn't be interested to turn them into a fair material. In case you are looking for further enhancement of quality, this is a good point to go completely digital. This part is nothing enigmatic. Here, it's not so much about thinking, but about leveraging the digital tools at hand to achieve the best result. This article is not about that, nor do I profess to be an expert in that topic. Hence, I will be brief here.


Stepwise Transformation

As mentioned earlier, it would be great if the hand-drawn figures can be evolved into digital figures. This would primarily mean replacing wobbly lines and irregular shapes with smooth and regular digitally created lines and shapes. If your handwriting is not good, that can be replaced by digital text. Also, reshaping, resizing and relocating components of your drawing would give satisfying results.

Once the basic idea is put down, gradual transformation to a more digital look begins

 

Of course, that's possible only when the first drawings are made digitally using an appropriate tool. Otherwise, you would have to put in the effort of creating the first digital equivalent of your hand-drawn figure in a traditional medium. In my understanding, that's a very small part of the overall effort.

My knowledge of Windows and Mac world is limited and dated, as it's been years since I last used them in any significant measure. But, I am very sure that there must be very powerful and flexible tools out there which allow you to gradually and stepwise turn your hand-drawn sketches into digitally rendered ones. May be even Microsoft Word and Powerpoint have these features. In Linux, I have recently been using Openboard and Xournal++ for this. Both are extremely stable, user friendly systems with very little learning curve involved. Again, it's not about the tools, but about a bit of ingenuity.

Nearly completely Digitised Figure; notice the seamless intermixing of handwritten characters with digitally drawn shapes


LaTeX and TikZ

TikZ typesetting is famously LaTeX Quality -- Unmatchable!

 

As for my personal taste, I do most (probably all) of my documentation in LaTeX. Hence, translating things into that environment works best for me. TikZ is a LaTeX based drawing tool. The advantage of using TikZ is that your drawings are a part of your LaTeX document. This helps their maintenance. The output quality is one of the best in my opinion. The figure is really drawn logically using a programming language of sorts, and not visually. This makes the whole thing very robust. Moving and resizing never messes up your figures. And staying within the LaTeX ecosystem has plenty of advantages which any seasoned LaTeX user will swear by. Some of them are as follows:

  1. Powerful. Almost anything can be drawn.
  2. Logically/mathematically defined structure which doesn't go wrong easily
  3. Vector graphics rendering. So no pixellation of figures when zooming in or printing out
  4. Powerful packages for virtually any kind of drawing use case
  5. Extremely active online community. So, nearly all questions get readily answered with a single Google search.

The only disadvantage of using this tool is the learning curve, which is quite formidable. In fact, without assuming a fair degree of familiarity with LaTeX as a prerequisite, it can be a difficult proposition to opt for TikZ as preferred drawing tool.


Conclusion

Writing mainly serves two purposes: as a thinking tool and as a communication/documentation/recording/archival tool. The former asks for flexibility. Handwriting is best for this. The latter asks for quality. Digital helps there. In this article, we have presented a process of documentation that begins with predominantly handwriting, and gradually transforming into digital depending on the specific quality requirements and resources available. We have also shared information about some tools which can be used to implement this process.