Using Minimal Arrow Functions
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 since this constraint appears highly restrictive, it will act as a barrier, not a 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 understand 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 solution they create. The programmer’s depth of understanding must extend to both their problem domain and the language(s) used to create that software solution.
- 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 pretty much any computing problem.
A Minimal Starting Point
Here, an anonymous function is created that receives a parameter
n and returns the value
n+1. In order to access this anonymous function later, we’ll store it in the variable
Ok, but that’s not exactly a ground breaking concept 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.
Well, OK. It’s a pretty clumsy way of counting, but I suppose it works.
You’re right, 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 it is, in spite of the awkward usage, completely correct.
Incidentally, the above nested calls to
incrare exactly how the Clojure function
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 of deriving the counting numbers. 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: that is, we cannot 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 an outer function that takes two parameters:
- The first parameter is a function that when called, performs a transformation on 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 a “plus one” style transformation on that value. Instead, we are describing a more abstract form of functionality.
We are saying that we need a function that receives both an arbitrary starting value, and a transformation function. The transformation function is then applied a certain number of times in order to derive the required value.
If we now plugin in the specific starting value of zero, and use a transformation function that “adds one”, then we have a means of deriving the counting numbers.
Well that’s OK for deriving
3, but how about deriving
Remember that this abstract function will apply the transformation function a certain number of times to the starting value?
Q: If our starting value is zero, then how many times should we apply the transformation function?
A: Zero times – simples!
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 transformed into a sequence of partial functions that each take a single parameter.
The problem with functions that take multiple parameters is that you must know all the parameter values at the time the function is called, otherwise it won't operate correctly.
However, once you have transformed a function by this process known as "currying", you can call it even though you don't yet know all the parameter values.
When you pass a curried function some, but not all, of its parameters, you get back a partial function that takes whatever parameters are still missing.
To let’s apply this understanding to the
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
So lets create a generic
adder function (no, not the snake…)
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 adds one to it.
“So what?” you might be saying, “Aren’t we just back to where we started?“.
True enough, the
incr function behaves no differently that it did 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 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:
You can think of the function names
TWO as representing the number of times the transformation function is applied to the starting value. So if we supply
incr as the transformation function and
0 as the starting value, then we have created a means of generating the counting numbers.
Now, put your brain into deep think mode and ask yourself this question…
What exactly, do the functions
At first glance you might think, “Oh they generate the counting numbers”.
Well yes, and then at the same time, no…
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 passed in the transformation function
incr and starting value of zero, and under these (and only these) circumstances 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.
So here, function
TWO applies the
incr function twice to the starting value of
5; hence the final result
But we could equally well create a decrement function and apply that to the value 5 like this:
Hmmm, that looks like it could be quite versatile.
Speaking of string concatenation, let’s create a function that actually does this correctly.
To make use of
str_concatenate, we must tell it which character is to be concatenated. In this case, we’ll arbitrarily choose the character ‘+’.
Now, we can call our functions again, but instead of using
incr as the transformation function and a starting value of 0, we will perform our transformation using
str_concatenate('+') and a starting value of empty string…
Now we can see that the functions
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 do not implement 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.
All source code from the Mindshift blog series can be found on GitHub