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 2 / 2 -- too long for a single SO answer!)
Type Constraints
Sometimes we want to restrict the generic types we can use in a signature in some way or another. We might want a
maximum
function that can operate onNumbers
, onStrings
, onDates
, but not on arbitraryObjects
. We want to describe ordered types, ones for whicha < b
will always return a meaningful result. We discuss details of the typeOrd
in the Types section; for our purposes, its sufficient to say that it is meant to capture those types which have some ordering operation that works with<
.This description [^maximum-note] adds a constraint section at the beginning, separated from the rest by a right double arrow ("
=>
" in code, sometimes "⇒
" in other documentation.)Ord a ⇒ [a] → a
says that maximum takes a collection of elements of some type, but that type must adhere toOrd
.In the dynamically-typed Javascript, there is no simple way to enforce this type constraint without adding type-checking to every parameter, and even every value of each list.[^strong-types] But that's true of our type signatures in general. When we require
[a]
in a signature, there's no way to guarantee that the user will not pass us[1, 2, 'a', false, undefined, [42, 43], {foo: bar}, new Date, null]
. So our entire type annotation is descriptive and aspirational rather than compiler-enforced, as it would be in, say, Haskell.The most common type-constraints on Ramda functions are those specified by the Javascript FantasyLand specification.
When we discussed a
map
function earlier, we talked only about mapping a function over a list of values. But the idea of mapping is more general than that. It can be used to describe the application of a function to any data structure holding some number of values of a certain type, if it returns another structure of the same shape with new values in it. We might map over aTree
, aDictionary
, a plainWrapper
that holds only a single value, or many other types.The notion of something that can be mapped over is captured by an algebraic type that other languages and FantasyLand borrow from abstract mathematics, known as
Functor
. AFunctor
is simply a type that contains amap
method subject to some simple laws. Ramda'smap
function will call themap
method on our type, assuming that if we didn't pass a list (or other type known to Ramda) but did pass something withmap
on it, we expect it to act like aFunctor
.To describe this in a signature, we add a constraints section to the signature block:
Note that the constraint block does not have to have just one constraint on it. We can have multiple constraints, separated by commas and wrapped in parentheses. So this could be the signature for some odd function:
Without dwelling on what it does or how it uses
Monoid
orOrd
, we at least can see what sorts of types need to be supplied for this function to operate correctly.[^maximum-note]: There is a problem with this maximum function; it will fail on an empty list. Trying to fix that problem would take us too far afield.
[^strong-types]: There are some very good tools that address this shortcoming of Javascript, including in-language techniques such as Ramda's sister project, Sanctuary, extensions of Javascript to be more strongly typed, such as flow and TypeScript, and more strongly-typed languages that compile to Javascript such as ClojureScript, Elm, and PureScript.
Multiple Signatures
Sometimes rather than trying to find the most generic version of a signature, it's more straightforward to list several related signatures separately. These are included in Ramda source code as two separate JSDoc tags, and end up as two distinct lines in the documentation. This is how we might write one in our own code:
And obviously we could do more than two signatures if we chose. But do note that this should not be too common. The goal is to write signatures generic enough to capture our usage, without being so abstracted that they actually obscure the usage of the function. If we can do so with a single signature, we probably should. If it takes two, then so be it. But if we have a long list of signatures, then we're probably missing a common abstraction.
Ramda Miscellany
Variadic Functions
There are several issues involved in porting this style signature from Haskell to Javascript. The Ramda team has solved them on an ad hoc basis, and these solutions are still subject to change.
In Haskell, all functions have a fixed arity. But Javsacript has to deal with variadic functions. Ramda's
flip
function is a good example. It's a simple concept: accept any function and return a new function which swaps the order of the first two parameters.This[^flip-example] show how we deal with the possibility of variadic functions or functions of fixed-but-unknown arity: we simply use ellipses ("
...
" in source, "``" in output docs) to show that there are some uncounted number of parameters missing in that signature. Ramda has removed almost all variadic functions from its own code-base, but this is how it deals with external functions that it interacts with whose signatures we don't know.[^flip-example]: This is not Ramda's actual code, which trades a little simplicity for significant performance gains.
Any / *
TypeWe're hoping to change this soon, but Ramda's type signatures often include an asterisk (
*
) or theAny
synthetic type. This was simply a way to report that although there was a parameter or return here, we could infer nothing about its actual type. We've come to the realization that there is only one place where this still makes sense, which is when we have a list of elements whose types could vary. At that point, we should probably report[Any]
. All other uses of an arbitrary type can probably be replaced with a generic type name such asa
orb
. This change might happen at any time.Simple Objects
There are several ways we could choose to represent plain Javascript objects. Clearly we could just say
Object
, but there are times when something else seems to be called for. When an object is used as a dictionary of like-typed values (as opposed to its other role as aRecord
), then the types of the keys and the values can become relevant. In some signatures Ramda uses "{k: v}
" to represent this sort of object.And, as always, these can be used as the results of a function call instead:
Records
Although this is probably not all that relevant to Ramda itself, it's sometimes useful to be able to distinguish Javascript objects used as records, as opposed to those used as dictionaries. Dictionaries are simpler, and the
{k: v}
description above can be made more specific as needed, with{k: Number}
or{k: Rectangle}
, or even if we need it, with{String: Number}
and so forth. Records we can handle similarly if we choose:Record notation looks much like Object literals, with the values for fields replaced by their types. We only account for the field names that are somehow relevant to us. (In the example above, even though our data had an 'occupation' field, it's not in our signature, as it cannot be used directly.
Complex Example:
over
So at this point, we should have enough information to understand the signature of the
over
function:We start with the type alias,
Lens s a = Functor f ⇒ (a → f a) → s → f s
. This tells us that the typeLens
is parameterized by two generic variables,s
, anda
. We know that there is a constraint on the type of thef
variable used in aLens
: it must be aFunctor
. With that in mind, we see that aLens
is a curried function of two parameters, the first being a function from a value of the generic typea
to one of the parameterized typef a
, and the second being a value of generic types
. The result is a value of the parameterized typef s
. But what does it do? We don't know. We can't know. Our type signatures tell us a great deal about a function, but they don't answer questions about what a function actually does. We can assume that somewhere themap
method off a
must be called, since that is the only function defined by the typeFunctor
, but we don't know how or why thatmap
is called. Still, we know that aLens
is a function as described, and we can use that to guide our understanding ofover
.The function
over
is described as a curried function of three parameters, aLens a s
as just analyzed, a function from the generic typea
to that same type, and a value of the generic types
. The whole thing returns a value of types
.We could dig a bit deeper and perhaps make some further deductions about what
over
must do with the types it receives. There is significant research on the so-called free theorems demonstrating invariants derivable just from type signatures. But this document is already far too long. If you're interested, please see the further reading.But Why?
So now we know how to read and write these signatures. Why would we want to, and why are functional programmers so enamored of them?
There are several good reasons. First of all, once we become used to them, we can gain a lot of insight about a function from a single line of metadata, without the distraction of names. Names sound like a good idea until you realize the names chosen by someone else are not the name you would choose. Above we discussed the functions called "
maximum
" and "makeObj
". Is it helpful or confusing to know that in Ramda, the equivalent functions are called "max
" and "fromPairs
"? It's significantly worse with parameter names. And of course there are often language barriers to consider as well. Even if English has become the lingua franca of the Web, there are people who will not understand our beautifully written, elegant prose about these functions. But none of this matters with the signatures; they express concisely everything important about a function except for what it actually does.But more important than this is the fact that these signatures make it extremely easy to think about our functions and how they combine. If we were given this function:
and
map
, which we've already seen looks likethen we can immediately derive the type of the function
map(foo)
by noting that if we substituteObject
fora
andNumber
forb
, we satisfy the signature of the first parameter tomap
, and hence by currying we will be left with the remainder:This makes working with functions a bit like the proverbial "Insert Tab A into Slot A" instruction. We can recognize just by the shapes of our functions exactly how they can be plugged together to build larger functions. Being able to do this is one of the key features of functional programming. The type signatures make it much easier to do so.
Further Reading
This is the syntax some functional languages (most notably Haskell) use for their type signatures.
The last symbol represents the return type, while all the others represent the type of the parameters. The reason for the seemingly odd syntax has to do with the fact that Haskell is curried; all functions take 1 parameter and return a value. Multi-arity functions are made up of functions that return new functions. Anytime you see a
->
, that's function application. You could think of the arrow as a "black box" that takes 1 input, and gives 1 output. This is how I visualized it when I first started Haskell.For example:
Is the signature for a function that take a number and a list of generic
a
s, and returns a 2-dimensional list ofa
s. Note that in Haskell, this would represent a function that takes aNumber
, and returns a function that takes a list ofa
s, and returns a two-dimensional list ofa
s. You often don't need to worry about the currying behavior though. You can call the function as though it actually had 2 parameters.a
s in this case represent a generic input. We don't care about the type since the individual elements, presumably, are never used. If a letter appears in the signature without being associated with a typeclass restriction (more on typeclasses below), assume it means a generic parameter where we don't at all care about the type (like adding a<T>
to a signature in Java, then usingT
).Is the signature for a function that takes a function and an
a
, and gives back ab
. It appears to be a genericmap
method. If list's are members of theApply
typeclass, you could consider thata
in this case could be a list, andb
is a modified version of the list.In the second example, the part before the "thick arrow" represents a type restriction.
Apply f
means that in the rest of the signature,f
represents a type that's a member of theApply
typeclass (similar to an interface). Presumably, theApply
typeclass represents types capable of being applied, so anf a
is ana
(any type), but is restricted to types that can be applied. From the context, I would have to assume that functions are implicitly members of theApply
typeclass, since they can be applied, and the above signature precedes the function parameter ((a -> b)
), with anf
.This part:
Represents a function that takes an
a
, and turns it into ab
; but in either case we don't care about what typea
orb
actually are. Because there are parenthesis around it, it represents a single function being passed. Anytime you see a signature with something like(a -> b)
, it means it's a signature for a Higher-Order Function.Suggested reading:
Understanding Haskell Type Signatures
From the Ramda Wiki:
(Part 1 / 2 -- too long for a single SO answer!)
Type Signatures
(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: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.
Named Types
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.
Here we have a simple function,
length
, that accepts a word, of typeString
, and returns the count of characters in the string, which is aNumber
. 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.
In this one, the function accepts two parameters, a position -- which is a
Number
-- and a word -- which is aString
-- and it returns a single-characterString
or the emptyString
.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:
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
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:Lists of Values
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:
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 ofString
s would be[String]
, a list ofBoolean
s would be[Boolean]
, a List of Lists ofNumber
s would be[[Number]]
.Such lists can be the return values from a function, too, of course:
And we should not be surprised to realize that we can combine these:
This function accepts a
Number
,val
, and a list ofNumber
s,nbrs
, and returns a new list ofNumber
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 ofNumber
s and return a list ofNumber
s.[^theorems][^theorems]: Well, there is other information we can glean, in the form of the free theorems the signature implies.
Functions
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.
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
accepts a tax rate, expressed as a percentage (typeNumber
, and returns a new function, which itself accepts aNumber
and returns aNumber
. Again, we describe the function returned by(Number → Number)
, which makes the signature of the whole functionNumber → (Number → Number)
.Currying
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 likemakeTaxCalculator
if that's what you wanted, but could also be used in a single pass: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 finalNumber → Number
part of the signature. When we callcalculateTax(8.875, 49.95)
, we have supplied the first twoNumber
parameters, and so get back only the finalNumber
.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: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 aString
. The first function it accepts itself takes aBoolean
and aNumber
and returns aString
. 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 anObject
and returns aBoolean
, and the third accepts anObject
and returns aNumber
.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
andg = curry(f)
, theng(a)(b)(c) == g(a)(b, c) == g(a, b)(c) == g(a, b, c) == f(a, b, c)
.Type Variables
If you've worked with
map
, you'll know that it's fairly flexible:From this, we would want to apply all the following type signatures to map:
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
, andObject
, 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:
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 (thinkk
andv
forkey
andvalue
orn
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 botha
andb
point toNumber
. This is not a problem. We still have two different type variables since there will be cases where they are not the same.Parameterized Types
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 arbitraryBox
; each one can only hold one sort of item. When we discuss aBox
we always need to specify aBox
of something.This is how we specify a
Box
parameterized by the unknown typea
: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
orBox 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 writtenDictionary 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.
Type Aliases
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:The aliases
Name
andUrl
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: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.)