Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Where can I find an explanation/summary of symbols used to explain functional programming, specifically Ramda.js?

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.

like image 259
Andrew Willems Avatar asked Nov 01 '16 13:11

Andrew Willems


1 Answers

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:

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.

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.

// 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

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:

// 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 Numbers. 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 Strings would be [String], a list of Booleans would be [Boolean], a List of Lists of Numbers 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 Numbers, nbrs, and returns a new list of Numbers.

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 Numbers and return a list of Numbers.[^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.

// 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).

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 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).

Type Variables

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.

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 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.

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:

// 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 Lenses, 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.)

like image 146
Scott Sauyet Avatar answered Sep 23 '22 00:09

Scott Sauyet