Pointfree/Tacit Programming in JavaScript
TweetIntroduction
Usually when we define a function, we specify the inputs of the function as parameters. Parameters are specified as a list of comma separated variable names. These variable names can be used in the function body. Point-free is a different way of defining functions. In Point-free form, the inputs to the function are implicit and parameter list is not specified. This is also known as tacit programming since the function definition is silent on the number of parameters and their names. Consider following trivial example of a function definition.
const A = (x)=>B(x) // (1)
Here, A
is defined as a single argument function and it is defined using lambda function syntax in JavaScript. The function body calls B
function and also passes the single parameter x
to function B
. This function definition is not point-free as it mentions explicitly that function takes a single argument and the argument name is x
. This function definition can also be written in point free form as follows
const A = B // (2)
Above definition is just as good as the first definition since B
is also a function which takes a single parameter and A
is also a function taking the same parameter. There is no need to explicitly mention the parameter x
here. This particular reduction from definition (1) to definition (2) is formally called eta-reduction in lambda calculus.
Basic Patterns
We have already seen a trivial function defined in point-free form. But what other types of functions can be defined in this form? Can all types of functions be defined in point-free form? It turns out that, there are five basic patterns and given the solutions for those patterns, we can convert any function definition into point-free form. Let us go over these patterns one by one.
1. Eta-Reduction
First and the most fundamental pattern is the eta-reduction. Conversion to point free form is done step by step and at each step right most variable in the method’s parameter list is removed. In order to drop the right most variable we need to be able to express the body of the method in a form similar to definition (1) and after that we can apply eta-reduction and drop the right most variable. For example, suppose we have following function definition
const A = (x,y)=>F(x,G(y))
Now, in order to remove y
, as it is the right most parameter, we need to first convert the definition in the form const A = (x,y)=>f(y)
where f=g(x)
. Right now, don’t worry about the details of how to obtain g
, as it will be explained later in this post. At this point it is important to remember that to express a definition in point-free form, it is first converted to a form similar to definition (1). After which we can apply the eta-reduction to get the definition without y
as follows
const A = x=>g(x)
Similarly, since this new definition is again in the form similar to definition (1), we can again apply eta-reduction and drop x
and obtain following definition
const A = g
This definition of A
is in point-free form as it does not mention any function parameter of A
2. Composition
Composition is one of most important tools when defining functions in point free form. To understand it, let us consider another example which involves two functions in the definition.
const F = (x)=>G(H(x)) // (3)
In order to convert definition (3) to point-free form, we need to introduce function composition. Function composition is a way to combine two functions X
and Y
to create a new function Z
such that result of applying Z
to an input a
is same as first applying Y
to a
and then applying X
to the result. As you can see that definition (3) matches this specification exactly. The function F
is defined as applying function H
first to x
and then applying G
to its result. Therefore, definition (3) can also be written in terms of composition of function G
and H
.
Many programming languages have in built notion of function composition. JavaScript does not have this built in, but we can create a function which can do same job for us. So, let’s define a function which can be used for function composition.
// Composition function C
const C = (f,g)=>(x=>f(g(x))) // (4)
The function C
above is a composition function which takes as input functions f
and g
, both taking single inputs themselves and returns a new function taking single input. Effect of calling this returned function is same as calling first g
on the input and then calling f
on the result. Using this composition function we can rewrite the definition (3) as follows.
const A = x=>C(G,H)(x) // (3.1)
As you can see that this definition is similar to the definition (1) and we can apply eta-reduction pattern and drop x
to convert it to point free form as follows.
const A = C(G,H) // (3.2)
3. Currying
Now let us consider another example of converting a function definition to point-free form.
const A = x=>H(G,x) // (5)
In definition (5) above, function A
is defined in terms of a function H
which takes two arguments. The first argument of function H
is a constant G
and the second argument is the parameter x
. In this case we cannot just drop x
, as H(G)
is not a function. What if we could rewrite H
such that instead of taking two parameters, H
takes first parameter and returns another function which takes which takes the second parameter. This quality of functions where invocation of the function with only first few arguments results in another function which can take remaining arguments is called currying. Now suppose function H
above supports currying so that we can invoke it using only a single argument. We can then write definition (5) as follows
const A = x=>H(G)(x) // (6)
In this definition we invoke H
providing only G
as argument. This causes H
to return another function which takes the second argument. Therefore, we invoke it by providing argument x
. This form of definition is again very similar to definition (1) and we again apply eta-reduction and drop x
as follows
const A = H(G) // (7)
The definition (7) is again in point-free form and the parameter x
is implicit. Since, we are talking about currying, let us also take this opportunity to re-define our composition function C
in definition (4). In this new definition it supports currying.
// New definition of composition function
const C = f=>g=>x=>f(g(x)) // (8)
Here, we are using JavaScript’s nested single parameter lambda function syntax to create the curried definition of composition function. Using the new definition (8) of composition function C
, we can rewrite definition (3.2) as follows
const A = C(G)(H) // (9)
Notice the subtle difference in definition (3.2) and (9). Instead of passing G
and H
as parameters to C
, we invoked C
with only G
first, which returns another function and we invoke it with H
which returns another function which is nothing but composition of G and H. From now on in this post, it is assumed that all functions are defined in this curried form.
4. Flip
As you already know by now that for converting definitions to point-free form we drop the right most parameter first. According to eta-reduction this parameter must also be the last argument of outer most function call in the body of function definition. Let us again consider example definition (6) but with parameters reversed.
const A = x=>H(x)(G) // (10)
In this case function H
takes x
as first argument and G
as second argument. In this case we cannot drop x
directly to convert it into point-free form. We first need to make sure that x
is the last argument of the outer most function call. This is a very common requirement as many times arguments are not in right order. It is useful to have a function which can rearrange the arguments of a function. This function is called flip
. This function takes a function g
as input and returns another function h
such that, invoking h
with arguments a
and b
is same as invoking g
with argument b
and a
. Let us first define the flip function
// Flip function definition
const F = g=>a=>b=>g(b)(a) // (11)
The above flip function definition is also fully curried as we mentioned in currying section. Now, using flip function F
we can re-write definition (10) as follows
const A = x=>F(H)(G)(x)
In this definition we pass F
the function H
, and it returns another function which when invoked with G
and x
has same result as invoking H
with x
and G
. Now if you notice, this definition is similar to definition (1) and we can drop x
and can write definition in point free form as follows
const A = F(H)(G)
5. Repeat (Dup)
Another important tool in conversion to point free form is Dup
function. Dup
function takes as input a function G
and returns another function which when invoked with an argument a
has same effect as invoking G
with two arguments, both of which are a
. Consider following example where we define square
function in terms of multiply function.
const mult = a=>b=>a*b
const square = a=>mult(a)(a)
In above definition, mult
function takes two parameters a
and b
and returns product of a
and b
. The square
function uses mult
in its definition by passing argument a
twice to mult
. Now, we cannot convert this definition to point-free because as per eta-reduction we need to drop right most parameter from both definition and parameter list, one by one. In this case definition has argument a
twice but parameter list has a
only once. So, we need to first equalize their number. This can be done with the function dup. Let us define the dup function.
const D = g=>a=>g(a)(a)
Here we define dup function D
. It takes a function g
as input and returns a function which takes a single argument which when invoked has same effect as invoking g
with two arguments, each of which is a
. Using this we can redefine square
as follows
const square = a=>D(mult)(a)
In this new definition we pass mult
to D
which returns a function which when invoked with a
has same effect of invoking mult
with a
for both its arguments. Now this form of definition is again similar to definition (1) and hence we can drop a
and convert it to point-free form.
const square = D(mult)
Dup function can also be used more than once to pass a single parameter any number of times. For example, consider following function
const print3 = a=>b=>c=>console.log([a,b,c])
To create a function which when invoked with parameter a
, pass that parameter to all three parameters of print3, we can use dup as follows
const A = D(D(print3))
That is, we use dup twice. In general case, to pass a parameter N times we use functionD
N-1 times.
Mastering the point-free form
Armed with basic patterns we are ready to convert any function definition to point free form. In order to get the hang of it, lets consider some examples
Example 1 : Simple two argument definition
Converting a simple two argument definition is not very different from the definition (1). Consider following definition.
const A = x=>y=>G(x)(y)
This can be converted to point-free form in two steps by applying eta-reduction in each step and dropping right most variable in each step
const A = x=>G(x) // step 1
const A = G // step 2
As you can see this is very similar to definition (1) conversion. Now lets try slightly more difficult example
Example 2: Two argument definition with two functions
const A = x=>y=>G(H(x)(y))
Here we have a definition where A
is defined in terms of two functions G
and H
. G
is a single parameter function and H
is a two parameter function. Function is defined as first calling H
with parameters x
and y
and then calling G
with the result. Now, in order to convert this to point free form, we need to first drop y
as it is the right most variable in the parameter list. In order to drop it we need to convert G(H(x)(y))
to the form k(y)
where k
is a function. As we know that H
is curried hence H(x)
is also a function, therefore G(H(x)(y))
is same as G(j(y))
where j
is H(x)
. This can be transformed to C(G)(j)(y)
using composition pattern. Therefore, substituting H(x)
back for j
we can rewrite the definition of A
as follows
const A = x=>y=>C(G)(H(x))(y)
in the above definition we can drop y as per eta-reduction to get following definition
const A = x=>C(G)(H(x))
Now again we can apply composition pattern and think of C(G)(H(x))
as f(g(x))
where f=C(G)
and g=H
and we can use composition to convert f(g(x))
to C(f)(g)(x)
and make x
drop-able
const A = x=>C(C(G))(H)(x) // or
const A = C(C(G))(H)
In this case we have successfully converted the definition to point-free from. We can also generalize this further so that any definition of form G(H(x)(y))
can be re-written as P(G)(H)(x)(y)
where P
is defined as follows
const P = G=>H=>C(C(G))(H)
Now as you can see P
itself is not point-free. Although, we are done with converting function A
to point-free form, as an exercise, lets also try to write P
as well in point-free form. First thing to note is that we can right away drop H
so that
const P = G=>C(C(G))
Now C(C(G))
is same as f(g(x))
and can be made point-free using composition pattern as C(f)(g)(x)
as follows
const P = G=>C(C)(C)(G)
const P = C(C)(C)
We can say that P
is another pattern, just like five basic patterns we discussed above. But P
is not a basic pattern as it can itself be arrived at using basic patterns. Learning such more complex patterns makes point free conversion quicker.
Example 3. Two argument function with three functions
const A = x=>y=>G(H(x))(I(y))
In order to convert this to point-free we first need to drop y
. Here again we can first apply composition pattern of y=>f(g(y))
where f=G(H(x))
and g=I
and make y
drop-able.
const A = x=>y=>C(G(H(x)))(I)(y)
This allows us to now apply eta-reduction pattern and drop y
as follows
const A = x=>C(G(H(x)))(I)
Now next step is to drop x
. But in order to drop x
we need to convert above definition in eta-reduction form such that A=x=>f(x)
. That is x
should be the last argument of the outer most function in the definition. Right now outer most function is C
and its second argument is I
. x
is contained in the first argument of C
. So, lets use flip pattern on C
to make I
first parameter as follows.
const A = x=>F(C)(I)(G(H(x)))
Now x
contained inside the second argument of top most function call but its still not the last argument of top most function call. We can apply composition pattern twice to make x
last argument. First we apply composition pattern x=>f(g(x))
such that f=G
and g=H
as follows.
const A = x=>F(C)(I)(C(G)(H)(x))
Now we can again apply composition pattern x=>f(g(x))
such that f=F(C)(I)
and g=C(G)(H)
as follows.
const A = x=>C(F(C)(I))(C(G)(H))(x)
This definition is now in eta-reduction form and we can drop x
to get the definition in point-free form
const A = C(F(C)(I))(C(G)(H))
Example 4: Repeated argument and three functions
Now we have used all patterns in our examples except the last pattern, dup. Lets consider following example as an exercise for using dup.
const A = x=>G(H(x))(I(x))
In this definition A
takes one argument x
. In the definition, this argument is first passed to H
and I
functions and then their return values are passed to a two argument function G
. In order to convert this to point free form we can execute similar steps as in previous example, but with a small twist. In the previous example we first dropped x
and then dropped y
. In this case we will execute all the steps but not drop any argument. Doing so will result in a form of definition as A= x=>f(x)(x)
once we obtain this form we can apply dup pattern. Lets first apply steps to make the second x
(the one that is argument of I
), the right most argument of outer function call. This step is exactly same as the step to move y
in previous example. So, I am not going into details of this conversion
const A = x=>C(G(H(x)))(I)(x)
Now, we can apply the steps we applied in previous example to make the first x
(the argument of H
) the last parameter of outer most function call. We can apply the same steps as in previous example to C(G(H(x)))(I)
part of the above definition as follows.
const A = x=>C(F(C)(I))(C(G)(H))(x)(x)
Now we have the definition in the form A=x=>f(x)(x)
where f=C(F(C)(I))(C(G)(H))
and we can apply dup as follows
const A = x=>D(C(F(C)(I))(C(G)(H)))(x)
Now, this definition is again in eta-reduction form and we can drop x
as follows
const A = D(C(F(C)(I))(C(G)(H)))
Now I have covered all the basic patterns we discussed in the form examples. I hope this gives you enough confidence to try your own examples and convert them to point-free form.
Conclusion
Point free form is a very pithy way to define new functions. In some cases definition even reads easier than regular definition especially if one is familiar with point free style and common patterns.
Conversion to point-free form can be done once we know the five basic patterns. We considered many examples and converted them to point-free form, although, I did not provide any proof that all forms of function definition conversions are indeed possible with just these five basic patterns.
In many cases, point free style can obfuscate the code and make it difficult to understand. I presented this style only as a novelty but in practice too much use of point-free form would lead to hard to understand code and should be avoided except in most trivial cases.