From the Ramda Wiki :
(Part 1/2 is too long for one SO answer!)
Type Labels
(or "What are these funny arrows?")
Looking at the documentation for the Ramda over
function, the first thing we see is two lines that look like this:
Lens sa -> (a -> a) -> s -> s Lens sa = Functor f => (a -> fa) -> s -> fs
For people coming to Ramda from other FP languages, they probably look familiar, but they can be pure gobbledy-gook for Javascript developers. Here we describe how to read them in the Ramda documentation and how to use them for your own code.
And in the end, as soon as we understand how these works, we will explore why people would like them.
Named types
Many ML languages, including Haskell , use the standard method for describing the signatures of their functions. As functional programming becomes more common in Javascript, this signature style is gradually becoming almost standard. We occupy the Haskell Version for Ramda.
We will not try to create a formal description, but simply the essence of these signatures with examples.
// length :: String -> Number const length = word => word.length; length('abcde'); //=> 5
Here we have a simple length
function that takes a word of type String
and returns the number of characters in the string, which is Number
. The comment above the function is the signature line. This starts with the name of the function, then the separator " ::
", and then the actual description of the functions. It should be clear enough that the syntax of this description. Function input is given, then arrow, then output. Usually you will see an arrow written as indicated above, " ->
" in the source code and as " β
" in the output documentation. They mean the same thing.
What we put before and after the arrow are the types of parameters, not their names. At this level of description, we all really said that this is a function that takes a string and returns a Number.
// charAt :: (Number, String) -> String const charAt = (pos, word) => word.charAt(pos); charAt(9, 'mississippi'); //=> 'p'
In this case, the function takes two parameters, the position - which is a Number
- and the word is String
- and it returns a one-character String
or an empty String
.
In Javascript, unlike Haskell, functions can take more than one parameter. To show a function that requires two parameters, we separate the two input parameters with a comma and wrap the group in parentheses: (Number, String)
. As in many languages, the Javascript function parameters are positional, so the order matters. (String, Number)
has a completely different meaning.
Of course, for a function that takes three parameters, we simply expand the list, separated by commas inside parentheses:
// foundAtPos :: (Number, String, String) -> Boolean const foundAtPos = (pos, char, word) => word.charAt(pos) === char; foundAtPos(6, 's', 'mississippi'); //=> true
And also for any larger finite list of parameters.
It would be instructive to note the parallel between the ES6 style arrow defining a function and declaration of this type. Function defined by
(pos, word) => word.charAt(pos);
Replacing the argument names with their types, the body with the type of the value that it returns, and the bold arrow " =>
", with the 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 the function to add all the numbers to the list, we could 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 of exactly what we mean by lists , but for now we can think of it as if they were arrays. To describe the list of this type, we wrap this type name in square brackets " [ ]
". The list from String
will be [String]
, the Boolean
list will be [Boolean]
, the list of Number
lists will be [[Number]]
.
Such lists can also be return values ββfrom a function:
// 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 that we can combine them:
// 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 takes Number
, val
and a list of Number
s, nbrs
and returns a new list of Number
s.
Itβs important to understand that this is all that the signature tells us. It is impossible to distinguish this function, only signatures, from any other function that accepts to accept Number
and the Number
list and return the Number
s list. [^ theorems]
[^ theorems]: Well, there is other information that we can get, in the form of free theorems implies a signature.
Functions
There is another very important type that we have not discussed. Functional programming is all functions; we pass functions as parameters and receive functions as the return value from another function. We must also introduce them.
In fact, we have already seen how we represent functions. Each signature line documented a specific function. We again use the small technique described above for 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 calc
function is described (Number β Number)
This is exactly the same as our top-level function signatures, just wrapped in brackets to correctly group it as a separate unit. We can do the same with the 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 the tax rate expressed as a percentage (type Number
and returns a new function, which itself takes Number
and returns a Number
. Again, we describe the function returned (Number β Number)
, which makes the signature of the entire function Number β (Number β Number)
Carring
Using Ramda, we probably won't write a makeTaxCalculator
exactly like this. Currying is central to Ramda, and we will probably take advantage of it here [^ curry-desc]
Instead, in Ramda, most likely write a calculateTax
curry function that can be used just 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 curry function can be used either by feeding both parameters up to the front and returning a value, or by supplying only one and getting a function that is looking for the second to return. To do this, we use Number β Number β Number
. In Haskell, ambiguity is resolved quite simply: the arrows snap to the right, and all functions take one parameter, although there is syntactic sleight of hand for which you can call them several parameters.
In Ramda, ambiguity is not permitted until we call a function. when we call calculateTax(6.35)
, since we decided not to provide the second parameter, we return the final part of the Number β Number
of the signature part. When we call calculateTax(8.875, 49.95)
, we set the first two parameters to Number
, and so we get only the final Number
.
Signatures of curry functions always look like this: sequence Types separated by " β
". Since some of these types may themselves be functions, there may be parenthesized substructures that themselves have arrows. That would be perfectly acceptable:
// someFunc :: ((Boolean, Number) -> String) -> (Object -> Boolean) -> // (Object -> Number) -> Object -> String
It is made up. I do not have a real function to point this out. But we can find out a fair bit about such a function from our type signature. This takes three functions and an Object
and returns a String
. the first function that it takes itself takes Boolean
and a Number
and returns a String
. Note that this is not described here as curry (or it would be written as (Boolean β Number β String)
.) The second parameter of the function takes Object
and returns a Boolean
, and the third takes Object
and returns a Number
.
This is a bit more complicated than realistic in Ramda's features. We do not often have functions of four parameters, and we, of course, do not have those that take three parameters of the function. Therefore, if this is clear, we are well on the path to understanding that Ramda should abandon us.
[^ curry-desc]: for people coming from other languages, Ramda currying may be slightly different from what you used: 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)
.
Type variables
If you worked with map
, you will know that it is quite 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 like to apply all of the following type signatures to the map:
// map :: (String -> String) -> [String] -> [String] // map :: (String -> Number) -> [String] -> [Number] // map :: (Number -> Number) -> [Number] -> [Number] // map :: (Number -> Boolean) -> [Number] -> [Boolean]
But it is clear that there are still many possibilities. We cannot just list a mall. To handle this, type signatures deal not only with specific classes such as Number
, String
and Object
, but also with the representation of generic classes.
How would we describe map
? It is pretty simple. The first parameter is a function that takes an element of one type and returns an element of the second type. (The two types do not have to be different). The second parameter is a list of elements of the input type of this function. It returns a list of elements of the output type of this function.
Here is how we could describe it:
// map :: (a -> b) -> [a] -> [b]
Instead of specific types, we use common placeholders, single letters of the lower character to indicate arbitrary types.
It is easy to distinguish them from specific types. These are complete words and are capitalized by agreement. Common type variables are just a
, b
, c
, etc. Sometimes, if there is a good reason, we could use the letter later in the alphabet if it helps the sense of which types can represent common ones (think k
and v
for key
and value
or n
for number), but basically we just use these from beginning of the alphabet.
Note that after a generic type variable is used in the signature, it represents a value that is fixed for all uses of the same variable. We cannot use b
in one part of the signature, and then reuse it in another place if both of them must be of the same type in the whole signature. Moreover, if the two types in the signature should be the same, then we have to use the same variable for them.
But there is nothing to say that two different variables sometimes cannot point to the same types. map(n => n * n, [1, 2, 3]); //=> [1, 4, 9]
map(n => n * n, [1, 2, 3]); //=> [1, 4, 9]
(Number β Number) β [Number] β [Number]
, therefore, if we must match (a β b) β [a] β [b]
, then both a
and and b
point to Number
. It's not a problem. We still have two type variables, as there will be times when they do not match.
Parameterized Types
Some types are more complicated. We can easily imagine a type representing a collection of similar items, let's call it Box
. But not a single instance of an arbitrary Box
; each can contain only one item. When we discuss Box
we always need to indicate Box
of something.
So we specify a Box
, parameterized by an unknown type a
: Box a
. This can be used wherever we need a type, as a parameter, or as a function return. Of course, we could parameterize the type using a more specific type, Box Candy
or Box Rock
. (Although this is legal, we are not actually doing this in Ramda at the moment. Maybe we just don't want to be blamed for being as stupid as a crate of rocks.)
There cannot be only one type parameter. We can have a Dictionary
, which is parameterized both by the type of keys and the type of values ββused. This can be written Dictionary k v
. It also demonstrates the place where we could use single letters that are not original from the alphabet.
There are not many declarations in Ramda, but we can often use such things in user code. Their greatest use is type support, so we need to describe those.
Type Aliases
Sometimes our types get out of hand, and it becomes difficult to work with them because of their intrinsic complexity or because they are too generic. Haskell allows you to use type aliases to simplify the understanding of these. Ramda also borrows this concept, although he used it sparingly.
The idea is simple. If we had a parameterized User String
type, where the String should represent the name, and we wanted to be more specific to the String type that was presented when creating the URL, we could create an alias of the type:
// 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
displayed to the left of " =
". Their equivalent values ββare displayed on the right.
As already noted, this can also be used to create simple aliases for a more complex type. A number of functions in Ramda work with Lens
es, and the types for them are simplified using a type alias:
We will try to break this complex value a little later, but for now, it should be clear enough that no matter what Lens sa
represents, underneath it is just an alias for the complex expression, Functor f β (a β fa) β s β fs
.
(Part 2 in a separate answer .)