A fundamental part of programming is how we document the code we write. Today we’ll look at a way to document functions in JavaScript.
Last week we saw how to compose functions in JavaScript. As we mentioned, once you create a function, what matters most is how it relates to other functions, not so much its implementation.
To understand this relationship, we need to think about the data types that functions receive and return.
In JavaScript, values are what have types, not variables — Types in JavaScript
JavaScript doesn’t have a strong type system for its variables. To check types, you could use a library like Flow.
Today we’ll look at a way to document functions using a system from functional programming that’s pretty simple and turns out to be very useful when reading code.
Hindley-Milner
This is a type system created for Lambda Calculus, used in functional languages, mainly Haskell.
Let’s see how we can document our functions using this system.
Hindley-Milner is a fairly simple notation that gives us clarity on how a function behaves with respect to its types.
Remember that types are basically a collection of values. For example, if we’re talking about the Boolean type, it’s a collection of two values (True, False).
Let’s start by looking at how basic types are written.
Basic Types
Bool — booleanos
Char — Caracter sencillo
String — Cadena de caracteres
Integer — Números enteros
Float — Números con punto flotante
List Types
Lists are sequences of values and are fundamental in any functional language (JavaScript isn’t a purely functional language, so lists are a special kind of object).
Arrays
Arrays are collections of a single type. To write them, we can use the type that composes them regardless of the length.
// xs :: [Bool]
const xs = [False, True, False, True]
// ns :: [Char]]
const ns = ['a', 'b', 'c']
We can also write lists of lists:
xss :: [[Char]]
const xss = [['a'], ['a','c']]
Arrays are typically written with names like xs, ns, xss
Tuple Types
JavaScript doesn’t have a tuple type, but it’s an important data type.
A tuple is a sequence of values of different types. We can use them to describe the data types that a function accepts.
// (Bool, Bool)
(False, True)
// (Bool, Char, Integer)
(False, 'a', 12)
Now that we’ve covered the data types typically used in functions, let’s see how to document them.
Documenting Our Functions
As we know, a function is a mapping from values of one type to values of another type (or the same type). We can write this relationship using an arrow that represents the function.
// not :: Bool → Bool
const not = (a) => !(a);
// isDigit :: a → Bool
const isDigit = (x) => typeof(x) === 'number'
// add :: (Int, Int) → Int
const add = (x,y) => x + y
// take :: ([a], Int) → a
const take = (xs, i) => xs[i]
// length :: String → Number
const length = (a) => return a.length
As you can see, documenting a function is pretty straightforward and gives us information about what types the function accepts and returns.
For example, the add function receives a tuple of two numbers and returns a number.
But if we look at the isDigit function, we see something interesting: its input type is written as a. That’s because this function doesn’t receive a specific type — it can receive various types.
Let’s explore this a bit further.
Polymorphic Functions
// length :: [a] → Int
const length = (xs) => xs.length();
length([true, false, true]); // 3
length([1,2,3,4,5]); // 5
// id :: a → a
const id = (x) => x
id('a'); //a
id(12); //12
The id function can receive values of any type, so we write its type with a. Type variables are written with letters like a, b, c, etc.
A type variable must start with a lowercase letter, while fixed types start with an uppercase letter.
Curried Functions
As we know, we can write curried functions, so we can document them as follows:
// add :: Int → Int → Int
const add = x => y => x + y
// map :: (a → b) → [a] → [b]
const map = curry(function(f, xs) {
return xs.map(f);
});
// reduce :: (b -> a -> b) -> b -> [a] -> b
const reduce = curry(function(f, x, xs) {
return xs.reduce(f, x);
});
The value that the function returns is the last one, and we can group functions from right to left.
The map and reduce functions are a bit more complex than the previous ones, but with this documentation, their purpose becomes clear.
Overloading a Function
A polymorphic function is called overloaded if its type contains one or more constraints.
For example:
sum :: Num a => (a, a) → a
In this sum function, before the arrow (=>) we’re clarifying that type a must be a numeric type.
This way, we’re putting a condition on which types can be used.
sum [1,2,3]
sum ['a','b','c'] -> error
Some of the constraints you can use are:
Num, numeric type
Eq , type that can compare equality
Ord, type that can be ordered
Conclusion
This documentation system is used in the functional programming community. If you read the Ramda documentation, you’ll see it uses this notation to document its methods.
While this notation doesn’t have an implementation in JavaScript, we can use it as documentation that will give us important information when composing functions.