When a Javascript Developer starts to explore the deepest secrets of the Functional Programming, he often finds these weird arrow notations with type written above the functions and feels ‘What the hell is this?’. After all, he’s a master of dynamic typed Javascript, free from the boundaries of types.
These type notations are a meta language called Type Signatures that tells a lot about a Pure Function and are important in functional programming much more than we might first expect.
Screenshot from http://ramdajs.com/docs/
Let’s see what these Type Signatures are and why we should use them in our code.
A Type Signature defines the inputs and outputs for the function, sometimes including the number of arguments, the types of arguments and order of arguments contained by a function.
These type signatures are highly expressive statements written above Pure Functions and are used to deduce it’s working.
Type Signatures are based on Hindley-Milner Type system as a standard type system which is also followed by ML-influenced languages, including Haskell.
These statements serves a greater purpose by Formalizing the functional expression in Type Inferring Algorithms_(commonly used by Haskell)_, but for now we’ll use them to document our Javascript code better and deriving Free Theorems out of them.
And if you find any Pure Function documented with these Type Signatures, the power to decode them will give you an ahead-of-time insight on the workings of that Function.
Just like He-man
We’ll be defining Type Signatures as comments above our functions. You can also use Flow to infer types when using functions. To get started with Flow you can follow this,
Type Checking with Flow_JavaScript maybe the fast, expressive, light-weight, functional, awesome, programming language, with a huge community…_medium.com
// length :: String → Numberconst length = s => s.length;
So the above function takes a string and returns a number. If we look closely we can see
1. The function name is written first followed by `::`.
2. The input type of the function is written before the arrow.
3. The return type of the function is written after the arrow or at last.
Remember, only the types of the input and output is written so that it can be read as “A function length from string to number”.
The above length function can also be written as
// length :: [Number] → Numberconst length = arr => arr.length
And it’s normal for a function to have a multiple type signatures as long it is practical, if a function is too flexible with its parameter types then we should use HM-arbitrary variables, we’ll discuss them in a sec.
In Javascript, we can have functions with many parameters, unlike other FP languages, and it’s a good practice to curry those functions to take only one parameter at a time. But, if we still want to use multiple parameters in our functions we can do this.
// join :: (String, [String]) → Stringconst join = (separator, arr) => arr.join(separator)
It’s not functional programming if we don’t have functions working on functions.
// addOneToAll :: ((Number → Number),[Number]) → [Number]
const addOneToAll = (addOne = x=>x+1 , arr) => arr.map(addOne)
When a function is passed as parameter, we wrap it’s signature in a parentheses to present a more meaningful overall Type Signature.
The above function is a ‘map’ function, not every time this function works on defined data types, basically, it can work on any type of array. So to describe these kinds of a function we need something else.
Functions like identity, map, filter and reduce accepts arguments that are too flexible to be defined by a specific type so we use classic Hindley-Milner variables _a_
and _b_
.
// identity :: a → aconst identity = a => a
Since identity will always be giving us the same output type for the same input type. Therefore, we used a → a
to represent it’s signature.
I know!
And also our length function can be written as
// length :: [a] → Numberconst length = arr => arr.length
Similarly,
// head :: [a] → aconst head = arr => arr[0]
_Type Signatures of the Purest of the Pure functions_✨
For a function taking multiple arguments, its always a good choice to curry them so that they can be composed, later in our code. Also, it’s not a good practice to use arbitrary HM’s variables with functions with multiple arguments.
If you are curious about Why we should curry our Functions please go through this,
Partial Application of Functions_Providing function with fewer arguments than it expects is called Partial Application of functions._hackernoon.com
// map :: (a → b) → [a] → [b]const map = fn => arr => arr.map(fn)
A standard map function will have the above type signature. But, many times map can also be defined by this type signature
map :: [a] → [b]
Sometimes we know the type of the array returned by map, like in this case.
// allToString :: [a] → [String]const allToString = arr => arr.map(toString)
Let’s look at standard filter and reduce
// filter :: (a → bool) → [a] → [a]const filter = fn => arr => arr.filter(fn)
// reduce :: (b
→ a
→ b)
→ b
→ [a]
→ b
const reduce = fn => init => arr => arr.reduce(fn, init)
Clearly, the type signature of a reduce function is a little complex, well if we can understand how to write the type signature of reduce function we can write the type signature for almost any function.
Okay, so the first argument of reduce is a reducing function that takes b
and a
to give b
this means that this function will be reducing everything to type b
, so the final value obtained from reduce() and the initial value provided will be of b
type. And since every single value from the list of type a
will be fed to this reducing function so the second argument of reducing function must be a
type, which it is. Therefore, the Type Signature of reduce() is justified.
Another use of these signatures is to produce Free Theorems. These theorems are highly useful when we are dealing with the Compositions of Pure Functions as they helps us in optimising and refactoring of our code.
// Type signature of head says// head :: [a] → a
compose(map(fn), head) == compose(head, fn)
This is our first free theorem, solely derived from the Type signature of head and map functions, which states
If we map
a function fn
on every element and then take the head
of the resultant array then that will be equivalent to applying the function fn
on head
of the array.
Let’s prove this Theorem
compose(map(fn), head) == compose(head, fn)
--Converting to Type Signatures--
[a] → [b] → b == [a] → a → b
-- Removing Intermediates --
[a] → b == [a] → b
Since the overall Type Signatures of both the functions are same, we can conclude that both composition will return same result for same input.
The above derivation is simplified as it requires Lambda Calculas for the actual derivation of free theorems which is not in the scope of this post.
You can always go through Wadler’s Paper on Free Theorems, if you want to dig deeper.
Please note that the Compose function used here is actually opposite of idiomatic Compose. More info here.
The power to decipher and use Type Signatures is useful not only in Javascript but also in other FP Languages, so if we need to borrow any Pure function to Javascript we can just refer to it’s Type Signature and we’ll know just where to put that function in our code.
Thanks for reading
Written with 💖