I am planning to use the Learning tag to create a series of posts about every online course I will take. The idea is to document everything in my blog. Having something that I can refer to is also super awesome.
Structure
The entire course is quite long. The way Dan (the instructor) has structured the course is that he divided the course into 3 parts.
In Part A we used the programming language SML (Standard Meta Language) since the primary focus of this part of the course is to learn about functional programming languages with static type checking (I will talk about these later). In Part B we use Racket and in Part C we use Ruby.
NOTE: haven't taken parts B and C of the course so I'm not too clear on that.
Part A : SML
I will write get to the point and write about each major topic that I learned.
Functional Programming and Pure Functions
This section defined functional programming for me. I have programmed a little bit in Scheme before (due to SICP) but I never really learned what Functional Programming really means. As a result I could not define it when someone asked me what it is.
Turns out that I never really understood pure functions, which is the core concept around which functional programming revolves. Now, pure functions are a mathematical concept. Mathematically a function just takes a value and returns a result. It does nothing else, or more specifically it changes nothing else. Moreover a function has to return the same result for the same argument always.
Well, how does this translate into programming ?
In programming terms, a pure function has to have no side effects Side effects is a term computer people use to confuse non-computer people. It means that a function should not mutate any mutable variables/references in the environment. This gets tricky sometimes, especially if the programming language allows and encourages mutations. Object-Oriented languages allow mutation and that can lead to some interesting problems (and security vulnerabilities) in software.
Essentially I wrote a lot of functional code for this course and its exercises and homework assignments. That was fun.
Static Typing
SML is a statically typed language, which means that the programs are type checked by the compiler before being run.
A typical program in SML with all types explicitly mentioned looks like this. Its a simple function that takes two numbers and adds them.
1 2 |
|
The type of the add
function is fn: int * int -> int
Let's write a recursive program that calculates n raised to m
.
1 2 3 4 |
|
The type of pow
function is fn: int * int -> int
I had some trouble understanding how the type system worked in SML, but things got quite clear after a bunch of exercises where we are required to write a very limited type checker ourselves. This was one of the best exercises in the entire course in my opinion. Turns out the type checker is just a massively logical piece of code and not magic and things do make sense if you know the type checking rules really well.
Type-Inference
In SML you are not required to explicitly mention the types of every expression, the type checker can infer the types itself. I found this very un-intuitive at first, but reading about the process made it quite obvious.
Let us rewrite the above functions without explicitly mentioning types.
1 2 3 4 5 6 7 |
|
In both these cases the type checker can infer the types of the
functions and the arguments and the final type for both the functions
is fn: int * int -> int
. The type checker can do this by looking
ahead
into the function and inspecting the operations being performed
on the arguments. The intuition being that when you check if an
argument value is equal to 0
or not, then the argument must be an
integer. Similarly, if you are adding two arguments, they must be
integers. To carry the idea forward, the static type checker would
throw an error if you tried to perform some operation on incompatible
types (like adding a string to an integer). This is also possible
because the operators are not overloaded like how in python the +
operator can do both addition for integers and concatenation for
strings. Having non-overloaded operators means that every operator has
a very specific type of operands that it accepts, and anything
otherwise would not type-check.
Polymorphic Data-types
Another thing that I found beautiful was the concept of polymorphic types in SML. In SML the type checker can figure out if there is a more-general way to write your program so that it can work for more than a single type of arguments. It does this even if you specify a type. What this means is that your programs become more general.
Let's write a function with polymorphic data-types.
1 2 |
|
The function add_to_list
builds a list by taking a object and a list
and adds the object to the front of the list. The type of this
function is fn: 'a * 'a list -> 'a list
where 'a
is a polymorphic
datatype as in it can take the type of any value passed to the
function. Hence this function works for both integers and strings.
Look the the results of the function calls in the comments.
1 2 3 4 5 6 7 8 |
|
It should be noted that this functions will only work if the type of
the object and the type of the object that the list is made up of are
the same. This is by definition since in SML the ::
operator works
like that. Hence the following call will not type check because we are
trying to add S
to the list [2,3]
.
1 2 |
|
Pattern Matching
Writing about pattern matching is tricky because it makes absolutely no sense without any context, and in order to establish context we would another series of blog posts which is not where I want to go with this. Let me try.
Imagine a case
expression. The utility of a case expression lies in
the fact that a case expression can handle multiple branches or
multiple cases.
The case expression in SML has the form
1 2 3 4 5 |
|
This is evaluated by first evaluating the expr_0
to val_0
and then
pattern-matching it with patt_i
and then based on which pattern it
matches with, the corresponding expr_i
is evaluated.
The pattern part is super cool because it lets you use constructors and operators within the pattern to pattern match against and you can also do variable binding within the pattern itself.
Let's see that in action. In the following program I will add all the elements of a list together.
1 2 3 4 5 6 7 8 |
|
In this example basically we are matching against the cases where the list is either empty or not. If the list is empty we return 0 as the sum, otherwise we use pattern matching to split the list into the first element and the rest of the list (without the first element) and then recursively compute the sum.
Lexical Scope and Closures
The body of a function is evaluated in the environment where the function is defined, not the environment where the function is called.
We will see it in this example.
1 2 3 4 5 |
|
The output of this function is 5
and not 9
because when the
function test
was defined, the value of x = 2
in the
environment. Hence when the function was called it used that value of
x as opposed the newer value where x is redefined to 4.
A function always has an environment associated with it. This environment contains the bindings at the time when the function was defined. This associating of a function and its environment (lexical environment) is called a function closure, or simply; a closure.
Closures are really cool. I did not know before but as it turns out all the fancy things that I like about Functional Programming like Currying, Partial Applications etc etc are just closure idioms.
There are still LOTS of details that I have not covered about pattern-matching.
Tail Recursion
After all this tail recursion is a relatively light-headed concept.
A tail call is a function call that is the very last operation in the calling function. i.e. the calling function has nothing to do after the function call and the result it receives from the function call is the final answer.
We will look at this using the factorial function. Mathematically we can define the factorial of a number as
factorial(n) = n * factorial (n - 1) ; when n > 1
We can write this in SML like,
1 2 3 4 |
|
Now, this is fine and it works; BUT, its not tail recursive.
This is because the calling function has to multiply the result from the recursive call with x
and then get the result.
Let's write the tail recursive version of this.
1 2 3 4 5 6 7 8 |
|
Over here we have a helper function that takes two parameters. The
first parameter stores the value of factorial(n - 1)
and the second
one keeps track of n
.
If we look at how we are calling the helper function recursively, we see that after the recursive call returns a value, we do not need to perform any operations on the result, since it is already the result we want. This is a tail call.
The benefits of tail calls is that compilers are smart enough to optimize tail calls and hence these recursive calls are not as expensive computationally.
Moreover, because of lexical scoping we can use any variable binding in the helper function without explicitly passing it during every call. This makes for very readable code. Using an accumulator to store the intermediate results also helps us avoid re-computation of already computed values. This is called memoization.
Wrapping up and Reflections.
This was by-far the most amazing course I have ever taken. I am 95% confident that this is exactly what I want to study going forward. It was quite challenging as well. Had a tough tiem figuring out work (I need to pay my bills :-P) and this course and a bunch of ofther commitments. I reckon I spent about 20 hours a week on this particular course. Anyway, super fun, can't wait for Part B.
Since I paid for this course (first online course I paid for) I got a certificate. My official final score is 95.1% which is good enough. This is my online certificate.
I am using the Gitlab repo Programming_languages_coursera to keep track of all my exercises and solutions. Currently I am keeping notes in a single large document with tons of comments. I haven't found a better way.
Its been a long time since I finished this course. I got detracted from my plan to start Part B ASAP.
I plan to revisit the challenge problems of week 2 and week 3 before starting Part B, which should take about a week more.