The API documentation for the JavaScript functional programming library Ramda.js contains symbolic abbreviations but does not provide a legend for understanding these. Is there a place (website, article, cheatsheet, etc.) that I can go to to decipher these?
Some examples from the Ramda.js API documentation:
Number -> Number -> Number
Apply f => f (a -> b) -> f a -> f b
Number -> [a] -> [[a]]
(*... -> a) -> [*] -> a
{k: ((a, b, ..., m) -> v)} -> ((a, b, ..., m) -> {k: v})
Filterable f => (a -> Boolean) -> f a -> f a
Lens s a = Functor f => (a -> f a) -> s -> f s
(acc -> x -> (acc, y)) -> acc -> [x] -> (acc, [y])
(Applicative f, Traversable t) => (a -> f a) -> t (f a) -> f (t a)
I am currently able to understand much of what Ramda.js is trying to do, and I can often make an educated guess what statements like the above mean. However I'm certain I would understand more easily if I understood these symbols/statements better. I would like to understand what individual components mean (e.g. specific letters, keywords, different arrow types, punctuation, etc.). I would also like to know how to "read" these lines.
I haven't had success googling this or searching StackExchange. I have used various combinations of "Ramda", "functional programming", "symbols", "abbreviations", "shorthand", etc. I'm also not exactly sure whether I'm looking for (A) universally used abbreviations in the broader field of functional programming (or perhaps even just programming in general), or (B) a specialized syntax that the Ramda authors are using (or perhaps co-opting from elsewhere but modifying further) just for their library.
From the Ramda Wiki:
(Part 1 / 2 -- too long for a single SO answer!)
(or "What are all those funny arrows about?")
Looking at the documentation for Ramda's over
function, the
first thing we see are two lines that look like this:
Lens s a -> (a -> a) -> s -> s
Lens s a = Functor f => (a -> f a) -> s -> f s
For people coming to Ramda from other FP languages, these probably look familiar, but to Javascript developers, they can be pure gobbledy-gook. Here we describe how to read these in the Ramda documentation and how to use them for your own code.
And at the end, once we understand how these work, we will investigate why people would want them.
Many ML-influenced languages, including Haskell, use a standard method of describing the signatures of their functions. As functional programming becomes more common in Javascript, this style of signatures is slowly becoming almost standard. We borrow and adapt the Haskell version for Ramda.
We will not try to create a formal description, but simply capture to the essence of these signatures through examples.
// length :: String -> Number
const length = word => word.length;
length('abcde'); //=> 5
Here we have a simple function, length
, that accepts a word, of type
String
, and returns the count of characters in the string, which is a
Number
. The comment above the function is a signature line. It starts
with the name of the function, then the separator "::
" and then the
actual description of the functions. It should be fairly clear what the
syntax of that description is. The input of the function is supplied,
then an arrow, then the output. You will generally see the arrow written
as above, "->
", in source code, and as "→
" in output
documentation. They mean exactly the same thing.
What we put before and after the arrow are the Types of the parameters, not their names. At this level of description, all we really have said is that this is a function that accepts a String and returns a Number.
// charAt :: (Number, String) -> String
const charAt = (pos, word) => word.charAt(pos); charAt(9, 'mississippi'); //=> 'p'
In this one, the function accepts two parameters, a position -- which is
a Number
-- and a word -- which is a String
-- and it returns a
single-character String
or the empty String
.
In Javascript, unlike in Haskell, functions can accept more than a single
parameter. To show a function which requires two parameters, we separate
the two input parameters with a comma and wrap the group in parentheses:
(Number, String)
. As with many languages, Javascript function
parameters are positional, so the order matters. (String, Number)
has
an entirely different meaning.
Of course for a function that takes three parameters, we just extend the comma-separated list inside the parentheses:
// foundAtPos :: (Number, String, String) -> Boolean
const foundAtPos = (pos, char, word) => word.charAt(pos) === char;
foundAtPos(6, 's', 'mississippi'); //=> true
And so too for any larger finite list of parameters.
It might be instructive to note the parallel between the ES6-style arrow function definition and these type declarations. The function is defined by
(pos, word) => word.charAt(pos);
By replacing the argument names with their types, the body with the
type of value it returns and the fat arrow, "=>
", with a skinny one,
"->
", we get the signature:
// (Number, String) -> String
Very often we work with lists of values, all of the same type. If we wanted a function to add all the numbers in a list, we might use:
// addAll :: [Number] -> Number
const addAll = nbrs => nbrs.reduce((acc, val) => acc + val, 0);
addAll([8, 6, 7, 5, 3, 0, 9]); //=> 38
The input to this function is a List of Number
s. There is a separate
discussion on precisely what we mean by Lists, but for now, we can
think of it essentially as though they were Arrays. To describe a List
of a given type, we wrap that type name in square braces, "[ ]
". A List
of String
s would be [String]
, a list of Boolean
s would be
[Boolean]
, a List of Lists of Number
s would be [[Number]]
.
Such lists can be the return values from a function, too, of course:
// findWords :: String -> [String]
const findWords = sentence => sentence.split(/\s+/);
findWords('She sells seashells by the seashore');
//=> ["She", "sells", "seashells", "by", "the", "seashore"]
And we should not be surprised to realize that we can combine these:
// addToAll :: (Number, [Number]) -> [Number]
const addToAll = (val, nbrs) => nbrs.map(nbr => nbr + val);
addToAll(10, [2, 3, 5, 7]); //=> [12, 13, 15, 17]
This function accepts a Number
, val
, and a list of Number
s,
nbrs
, and returns a new list of Number
s.
It's important to realize that this is all the signature tells us.
There is no way to distinguish this function, by the signature alone,
from any other function which happens to accept a Number
and a list of
Number
s and return a list of Number
s.[^theorems]
[^theorems]: Well, there is other information we can glean, in the form of the free theorems the signature implies.
There is still one very important type we haven't really discussed. Functional programming is all about functions; we pass functions as parameters and receive functions as the return value from other functions. We need to represent these as well.
In fact, we've already seen how we represent functions. Every signature line documented a particular function. We reuse the technique above in the small for the higher-order functions used in our signatures.
// applyCalculation :: ((Number -> Number), [Number]) -> [Number]
const applyCalculation = (calc, nbrs) => nbrs.map(nbr => calc(nbr));
applyCalculation(n => 3 * n + 1, [1, 2, 3, 4]); //=> [4, 7, 10, 13]
Here the function calc
is described by (Number → Number)
It is
just like our top-level function signatures, merely wrapped in
parentheses to properly group it as an individual unit. We can do the
same thing with a function returned from another function:
// makeTaxCalculator :: Number -> (Number -> Number)
const makeTaxCalculator = rate => base =>
Math.round(100 * base + base * rate) / 100;
const afterSalesTax = makeTaxCalculator(6.35); // tax rate: 6.35%
afterSalesTax(152.83); //=> 162.53
makeTaxCalculator
accepts a tax rate, expressed as a percentage (type
Number
, and returns a new function, which itself accepts a Number
and returns a Number
. Again, we describe the function returned by
(Number → Number)
, which makes the signature of the whole function
Number → (Number → Number)
.
Using Ramda, we would probably not write a makeTaxCalculator
exactly
like that. Currying is central to Ramda, and we would probably take
advantage of it here.[^curry-desc]
Instead, in Ramda, one would most likely write a curried calculateTax
function that could be used exactly like makeTaxCalculator
if that's
what you wanted, but could also be used in a single pass:
// calculateTax :: Number -> Number -> Number
const calculateTax = R.curry((rate, base) =>
Math.round(100 * base + base * rate) / 100);
const afterSalesTax = calculateTax(6.35); // tax rate: 6.35%
afterSalesTax(152.83); //=> 162.53
// OR
calculateTax(8.875, 49.95); //=> 54.38
This curried function can be used either by supplying both parameters up
front and getting back a value, or by supplying just one and getting
back a function that is looking for the second one. For this we use
Number → Number → Number
. In Haskell, the ambiguity is resolved
quite simply: the arrows bind to the right, and all functions take a
single parameter, although there is some syntactic sleight of hand to
make it feel as though you can call them with multiple parameters.
In Ramda, the ambiguity is not resolved until we call the function. When
we call calculateTax(6.35)
, since we have chosen not to supply the
second parameter, we get back the final Number → Number
part of the
signature. When we call calculateTax(8.875, 49.95)
, we have supplied
the first two Number
parameters, and so get back only the final
Number
.
The signatures of curried functions always look like this, a sequence of
Types separated by '→
's. Because some of those types might
themselves be functions, there might be parenthesized substructures
which themselves have arrows. This would be perfectly acceptable:
// someFunc :: ((Boolean, Number) -> String) -> (Object -> Boolean) ->
// (Object -> Number) -> Object -> String
This is made up. I don't have a real function to point to here. But we
can learn a fair bit about such a function from its type signature. It
accepts three functions and an Object
and returns a String
. The
first function it accepts itself takes a Boolean
and a Number
and
returns a String
. Note that this is not described here as a curried
function (or it would have been written as (Boolean → Number →
String)
.) The second function parameter accepts an Object
and returns
a Boolean
, and the third accepts an Object
and returns a Number
.
This is only slightly more complex than is realistic in Ramda functions. We don't often have functions of four parameters, and we certainly don't have any that accept three function parameters. So if this one is clear, we're well on our way to understanding anything Ramda has to throw at us.
[^curry-desc]: For people coming from other languages, Ramda's
currying is perhaps somewhat different than you're used to: If f ::
(A, B, C) → D
and g = curry(f)
, then g(a)(b)(c) == g(a)(b, c) ==
g(a, b)(c) == g(a, b, c) == f(a, b, c)
.
If you've worked with map
, you'll know that it's fairly flexible:
map(word => word.toUpperCase(), ['foo', 'bar', 'baz']); //=> ["FOO", "BAR", "BAZ"]
map(word => word.length, ['Four', 'score', 'and', 'seven']); //=> [4, 5, 3, 5]
map(n => n * n, [1, 2, 3, 4, 5]); //=> [1, 4, 9, 16, 25]
map(n => n % 2 === 0, [8, 6, 7, 5, 3, 0, 9]); //=> [true, true, false, false, false, true, false]
From this, we would want to apply all the following type signatures to map:
// map :: (String -> String) -> [String] -> [String]
// map :: (String -> Number) -> [String] -> [Number]
// map :: (Number -> Number) -> [Number] -> [Number]
// map :: (Number -> Boolean) -> [Number] -> [Boolean]
But clearly there are many more possibilities too. We cannot simply list
them all. To deal with this, type signatures deal not only with concrete
classes such as Number
, String
, and Object
, but also with
representations of generic classes.
How would we describe map
? It's fairly simple. The first parameter is
a function that takes an element of one type, and returns an element of
a second type. (The two type don't have to have to be different.) The
second parameter is a list of elements of the input type of that
function. It returns a list of elements of the output type of that
function.
This is how we could describe it:
// map :: (a -> b) -> [a] -> [b]
Instead of the concrete types, we use generic placeholders, single lower-character letters to stand for arbitrary types.
It's easy enough to distinguish these from the concrete types. Those are
full words, and by convention are capitalized. Generic type variables
are just a
, b
, c
, etc. Occasionally, if there is a strong reason,
we might use a letter from later in the alphabet if it helps makes some
sense of what sorts of types the generic might represent (think k
and
v
for key
and value
or n
for a number), but mostly we just use
these ones from the beginning of the alphabet.
Note that once a generic type variable is used in a signature, it
represents a value that is fixed for all uses of that same variable. We
can't use b
in one part of the signature and then reuse it elsewhere
unless both have to be of the same type in the entire signature.
Moreover, if two types in the signature must be the same, then we have
to use the same variable for them.
But there is nothing to say that two different variables can't sometimes
point to the same types. map(n => n * n, [1, 2, 3]); //=> [1, 4, 9]
is
(Number → Number) → [Number] → [Number]
, so if we're to match
(a → b) → [a] → [b]
, then both a
and b
point to Number
.
This is not a problem. We still have two different type variables since
there will be cases where they are not the same.
Some types are more complex. We can easily imagine a type representing a
collection of similar items, let's call it a Box
. But no instance is
an arbitrary Box
; each one can only hold one sort of item. When we
discuss a Box
we always need to specify a Box
of something.
// makeBox :: Number -> Number -> Number -> [a] -> Box a
const makeBox = curry((height, width, depth, items) => /* ... */);
// addItem :: a -> Box a -> Box a
const addItem = curry((item, box) => /* ... */);
This is how we specify a Box
parameterized by the unknown type a
:
Box a
. This can be used wherever we need a type, as a parameter or as
the return of a function. Of course we could parameterize the type with
a more specific type as well, Box Candy
or Box Rock
. (Although this
is legitimate, we don't actually do this in Ramda at the moment. Perhaps
we simply don't want to be accused of being as dumb as a box of rocks.)
There does not have to be just a single type parameter. We might have a
Dictionary
type that is parameterized over both the type of the keys
and the type of the values it uses. This could be written Dictionary k
v
. This also demonstrates the sort of place where we might use single
letters that are not the initial ones from the alphabet.
There aren't many declarations like this in Ramda itself, but we might find ourselves using such things fairly often in custom code. The largest usage of these is to support typeclasses, so we should describe those.
Sometimes our types get out of hand, and it becomes difficult to work with them because of their inner complexity or because they're too generic. Haskell allows for type aliases to simplify the understanding of these. Ramda borrows this notion as well, although it's used sparingly.
The idea is simple. If we had a parameterized type User String
, where
the String was meant to represent a name, and we wanted to be more
specific about the type of String that is represented when generating a
URL, we could create a type alias like this:
// toUrl :: User Name u => Url -> u -> Url
// Name = String
// Url = String
const toUrl = curry((base, user) => base +
user.name.toLowerCase().replace(/\W/g, '-'));
toUrl('http://example.com/users/', {name: 'Fred Flintstone', age: 24});
//=> 'http://example.com/users/fred-flintstone'
The aliases Name
and Url
appear to the left of an "=
". Their
equivalent values appear to the right.
As noted, this can also be used to create a simple aliases to a more
complex type. A number of functions in Ramda work with Lens
es, and the
types for those are simplified by using a type alias:
// Lens s a = Functor f => (a -> f a) -> s -> f s
We'll try to break down that complex value a little later, but for now,
it should be clear enough that whatever Lens s a
represents,
underneath it is just an alias for the complicated expression, Functor
f ⇒ (a → f a) → s → f s
.
(Part 2 in a separate answer.)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With