Mindshift: Part 4 Republished on Feb 1st, 2018 Previous Blog Next Blog Using Minimal Arrow Functions If you’ve read Part 3 of my Mindshift series of blogs, you should have an understanding of how the JavaScript operator called "arrow" or "fat arrow" => works. This operator allows you to create a JavaScript function using a minimal syntax. Let’s Look Into the Heart of Computation Now lets take a look at how we can build up a powerful set of functions using only the most basic arrow function syntax – but what do I mean by "the most basic arrow function syntax"? I mean that we will confine ourselves to using functions that take only one parameter, and contain only one expression. At first you might think that such a highly restrictive constraint could not possibly be of any benefit in the hard-nosed world of business programming. Why not just write code that works and be done with it? What use is there in performing extraordinary acts of mental gymnastics simply to overcome barriers of our own invention? Well, at a superficial level, that objection might seem plausible; but upon closer examination, you’ll discover this argument is usually based on the widespread unwillingness we humans have to question our own thought processes in an objective and clear-headed way. And in this particular case, this unwillingness leads to us missing the opportunity to gain a deeper understanding of the nature of computation. Two points should be made here: By confining ourselves to use only minimal functions as our building blocks, we force ourselves to examine the very nature of what it means to "compute" a solution. If we force ourselves to make this examination, then we will be able to create better computing solutions. There is a direct correlation between a programmer’s depth of understanding and the simplicity of the subsequent software they write. As a programmer, your depth of understanding must extend to both the problem domain within which you work, and the language(s) you use to solve problems. Shallow understanding leads to excessively complex solutions that in turn are both hard to debug and even harder to maintain. Deep understanding leads to simple(r) solutions that are much easier both to debug and maintain. A large part of the passion of programming is to master the thought processes that allow you to solve a problem in the simplest way possible; but as I’ve already described in a previous blog, "Simple" and "Easy" are not necessarily the same thing. And here we are going to run up against a perfect example of this. Back in Oct 1980, the British computer scientist Tony Hoare delivered the Turing Award Lectures. In his speech, he made the following poignant observation: I conclude that there are two ways of constructing a software design: One way is to make it so simple that there are obviously no deficiencies and the other way is to make it so complicated that there are no obvious deficiencies. The first method is far more difficult. It demands the same skill, devotion, insight, and even inspiration as the discovery of the simple physical laws which underlie the complex phenomena of nature. In other words, in a simple system deficiencies are self-evident; but complexity hides deficiencies. Therefore, wherever possible, complexity should be avoided. Our objective here is to follow Tony’s observation and create a software design so simple, that it obviously contains no deficiencies. However, we should bear his subsequent caveat in mind: in order to create such a solution, we must think deeply, rigorously and creatively about the problem. Only then will we arrive at a solution simple enough that it obviously contains no deficiencies. So, strap your brain down… I’m going to take you through another step of "Mindshift" as we look into a fundamental area of computing. What we discover will then allow us to build up our understanding of how to use functions as the basic building blocks from which we can start to solve a wide range of computing problems. A Minimal Starting Point So just as a quick recap, an arrow function is the bare bones syntax needed to declare a function in JavaScript. Such functions can either remain anonymous or can be assigned to some variable name:
// Increment a number incr = n => n + 1
Here, we assign an anonymous function to the name incr. This function receives a parameter n and returns the value n+1. Ok, but that’s not exactly a ground breaking stuff is it? Nonetheless, at least this arrow function meets our requirements: it takes one parameter and contains one expression. Using this minimal definition of an incr function, let’s now try to perform the most basic task possible with integers – counting through the natural numbers.
incr(0)
incr(incr(0))
incr(incr(incr(0)))
Well, OK. It’s a pretty clumsy way of counting, but I suppose it works. This is certainly neither elegant nor optimised; but, we what we have done is create a solution so simple that it obviously contains no errors. Just by looking at the definition of the incr function, and then at how this function is being used, we can easily determine that, in spite of the awkward usage, it is completely correct. Incidentally, the above nested calls to incr are exactly how the Clojure function iterate works. So we have created a software design that is "simple" according to Tony Hoare’s definition of the word. In this discussion, computational efficiency is not the issue. First and foremost, we want to write code that is correct; we’ll deal with the question of optimisation later. Is That As Simple As We Can Make It? It would be easy to stop at this point and think that since we have found a simple way of deriving the counting numbers, that we have therefore found the simplest way. However, as I said above, in order to move forward here, we need to be willing to examine our own thought processes – and then not run away from the discoveries that such an examination might reveal! In the above coding, we have created a situation in which a function is passed a starting value, and then using a sequence of nested calls, the result is passed into the same function multiple times in order to derive the required value. However, there is a problem with this design: in addition to the very clumsy usage, we have no way to derive the integer zero – at least not without resorting to the messy workaround of passing in a starting value of -1. And since computers start counting from zero instead of one, this is quite an omission. We need to look at how we can fix this. We can generalise the way the incr function is being used here by seeing it as the internal workings of a larger, more general purpose function that takes two parameters: The first parameter is a function that when called, transforms some starting value The second parameter is the value to be transformed Notice what we have not said here. We have not said that we will start from zero and then perform the transformation “plus one” on that value. Instead, we are describing a much more general purpose form of functionality. Our general purpose function receives two things: Some arbitrary starting value Some transformation function that can manipulate the start value. The transformation function is then applied a certain number of times to the start value in order to derive an answer. In order to use our general purpose function for counting, we must supply a value and a function that will generate the required result. So in the case of countring, we must supply: A start value of zero A transformation function capable of performing the “add one” operation Now our general purpose function has been "configured" (for want of a better term) to derive the counting numbers. Deriving Zero Well that’s OK for deriving the integers 1 or 2 or 3, but how can we derive 0? Remember that this general purpose function is designed to apply the transformation function a certain number of times to some starting value? Q: If our starting value is zero, then how many times should we apply the transformation function? A: Oh, that's easy - zero times! However, before we go charging ahead and create a function that takes two parameters, we must remember our self-imposed restriction that a function may only take one parameter. Hmmm, what to do… Currying and Partial Functions In order to create a function that takes two parameters, yet does not violate our self-imposed constraint that functions may only take one parameter, we need to perform a process called “Currying” (named after the logician Haskell Curry). Currying is a process in which a function that initially takes multiple parameters is refactored into a sequence of partial functions that each take a single parameter. Lets start with a function that takes two parameters. In order for this function to behave correctly, it must receive both parameter values at the time you call it.
(parm1, parm2) => parm2 + parm1
Let's pretend we are able to call this function, but pass it only one parameter - what would we end up with? If we set parm1 equal to 1 and then rewrite the function, we'd end up with:
parm2 => parm2 + 1
In other words, we get back another function in which the value of parm1 has been substituted with the supplied value, but the value of parm2 remains unknown. So we can rewrite the original function like this:
parm1 => parm2 => parm2 + parm1
This refactoring process is known as currying and the function is now said to have been curried. We now have the ability to create partial functions. A partial function is a function that has been supplied with some, but not all of its parameters. So let’s apply this understanding to the incr function. This function does nothing more than take a single parameter and add one to it. So in fact incr is nothing more than a special case of the addition operation where one of the operands has been hard-coded to 1. So lets create a generic adder function (no, not the snake…)
adder = val1 => val2 => val2 + val1
Ok, that looks simple enough, but how do we use it? Well, in the case of our incr function, we know that one of the values in our addition operation will always be the number 1. Therefore, if we call adder with only one parameter (the number 1), then we will get back a partial function that takes a single parameter and then adds one to it.
increment = adder(1)
increment(0)
“_So what?_” you might be saying, “_Aren’t we just back to where we started?_“. True enough, our new increment function behaves no differently than the original incr function before. But we have now analysed the problem right down to bare metal. We have recognised that in order to increment a value, we need to perform the arithmetic operation “add” where one of the parameters has been hard-coded to 1. In other words, we’ve followed Tony Hoare’s recommendation and produced a software system (albeit a trivial one) so simple that is obviously contains no errors. The point of partial functions is that they are designed to perform most, but not all of an operation. In this case, we have created an incr function by passing the general purpose adder function only one of its two required parameters. This results in a function that can add one to any subsequent value it receives. We will soon see that partial functions are very important and useful building blocks in functional programming. Generating the Counting Numbers(?) So now that we have a way to increment a value, we can create a set of functions that can calculate any of the counting numbers. In the previous section, we saw that any counting number can be generated by transforming a starting value a certain number of times. We also described the fact that if we want a function that generates the quantity zero, all we need to do (or not do) is apply the transformation function zero times. So we can now create some functions that generate the first few counting numbers:
ZERO = transform => start_value => start_value
ONE = transform => start_value => transform(start_value)
TWO = transform => start_value => transform(transform(start_value))
You can think of the function names ZERO, ONE and TWO as representing the number of times the transformation function is applied to the starting value. So if we supply increment as the transformation function and 0 as the starting value, then we have created a means of generating the counting numbers.
ZERO(increment)(0)
ONE(increment)(0)
TWO(increment)(0)
Now, put your brain into deep think mode and ask yourself this question… What exactly, do the functions ZERO, ONE, and TWO do? At first glance you might think, “Oh they generate the counting numbers”. Well yes - but that's only the superficial answer… Quantity Functions Above we saw that you should think of the names of these functions as representing the number of times the transformation function is applied to the starting value – and this is the key to understanding that these functions can do a lot more than simply generate counting numbers. Up until now, we have only ever supplied the transformation function increment and a starting value of zero, so it is only under these circumstances that can we generate the counting numbers. Lets illustrate this with some practical examples. Instead of passing TWO a starting value of zero, let’s pass it some other integer.
// Increment 5 using the function TWO TWO(increment)(5)
So here, function TWO applies the increment function twice to the starting value of 5; hence the result 7. But we could equally well create a decrement function and apply that to the value 5 like this:
decrement = adder(-1)
TWO(decrement)(5)
Hmmm, that looks like it could be quite versatile. So now, because this is JavaScript, let’s do something silly…
// Increment the character 'A' using the function TWO TWO(increment)('A')
This odd result is caused by JavaScript’s type coercion behaviour that leaps into action when you try to perform an arithmetic operation on an alphabetic character. Since this not a valid operation, rather than throwing its toys out of the pram, JavaScript allows the + plus operator to mean “string concatenation” instead of “arithmetic addition” (this is known as ‘overloading an operator’). Consequently, instead of trying to add the integer 1 to the character "A" (which is impossible), the integer 1 is interpreted as the character "1" and then concatenated to the starting value of "A". Speaking of string concatenation, let’s create a function that actually does this correctly.
// Concatenate a character to the end of a string // This function needs to remember the character being concatenated, // so we wrap the concat() function call within a partial function // that can remember the value of "char". // // "acc" is the accumulator into which the characters are // concatenated str_concatenate = char => acc => acc.concat(char)
To make use of str_concatenate, we must tell it which character is to be concatenated. In this case, we’ll arbitrarily choose the plus character ‘+’. Now, we can call our functions again, but instead of using increment as the transformation function and a starting value of 0, we will perform our transformation using str_concatenate('+') and a starting value of the empty string…
ZERO(str_concatenate('+'))('')
ONE(str_concatenate('+'))('')
TWO(str_concatenate('+'))('')
Now we can begin to see that the functions ZERO, ONE and TWO are much more versatile than simply a means for generating the counting numbers. They are generic functions that transform some starting value a given number of times. In other words, these functions can do much more than generate the integers 0, 1, and 2; instead, they implement the concept of a quantity such as zero-ness or one-ness or two-ness. Now using this somewhat more abstract idea of a quantity function, we can apply any transformation function to any starting value a given number of times. I trust this gives you the start of an insight into both the subtlety and power of the functional approach to programming. In the next Mindshift blog, we’ll develop the idea of how these abstract functions can be used to perform basic arithmetic operations. Enjoy! Chris W