Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
153 views
in Technique[技术] by (71.8m points)

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

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

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 on Numbers, on Strings, on Dates, but not on arbitrary Objects. We want to describe ordered types, ones for which a < b will always return a meaningful result. We discuss details of the type Ord 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 <.

// maximum :: Ord a => [a] -> a
const maximum = vals => reduce((curr, next) => next > curr ? next : curr,
    head(vals), tail(vals))
maximum([3, 1, 4, 1]); //=> 4
maximum(['foo', 'bar', 'baz', 'qux', 'quux']); //=> 'qux'
maximum([new Date('1867-07-01'), new Date('1810-09-16'),
         new Date('1776-07-04')]); //=> new Date("1867-07-01")

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

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 a Tree, a Dictionary, a plain Wrapper 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. A Functor is simply a type that contains a map method subject to some simple laws. Ramda's map function will call the map method on our type, assuming that if we didn't pass a list (or other type known to Ramda) but did pass something with map on it, we expect it to act like a Functor.

To describe this in a signature, we add a constraints section to the signature block:

// map :: Functor f => (a -> b) -> f a -> f b

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:

// weirdFunc :: (Functor f, Monoid b, Ord b) => (a -> b) -> f a -> f b

Without dwelling on what it does or how it uses Monoid or Ord, 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:

// getIndex :: a -> [a] -> Number
//          :: String -> String -> Number
const getIndex = curry((needle, haystack) => haystack.indexOf(needle));
getIndex('ba', 'foobar'); //=> 3
getIndex(42,  [7, 14, 21, 28, 35, 42, 49]); //=> 5

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.

// flip :: (a -> b -> ... -> z) -> (b -> a -> ... -> z)
const flip = fn => function(b, a) {
  return fn.apply(this, [a, b].concat([].slice.call(arguments, 2))); 
}; 
flip((x, y, z) => x + y + z)('a', 'b', 'c'); //=> 'bac'

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

We're hoping to change this soon, but Ramda's type signatures often include an asterisk (*) or the Any 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 as a or b. 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 a Record), 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.

// keys :: {k: v} -> [k]
// values :: {k: v} -> [v]
// ...
keys({a: 86, b: 75, c: 309}); //=> ['a', 'b', 'c']
values({a: 86, b: 75, c: 309}); //=> [86, 75, 309]

And, as always, these can be used as the results of a function call instead:

// makeObj :: [k,v]] -> {k: v}
const makeObj = reduce((obj, pair) => assoc(pair[0], pair[1], obj), {});
makeObj([['x', 10], ['y', 20]]); //=> {"x": 10, "y": 20}
makeObj([['a', true], ['b', true], ['c', false]]);
//=> {a: true, b: true, c: false}

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:

// display :: {name: String, age: Number} -> (String -> Number -> String) -> String
const display = curry((person, formatter) => 
                      formatter(person.name, person.age));
const formatter = (name, age) => name + ', who is ' + age + ' years old.';
display({name: 'Fred', age: 25, occupation: 'crane operator'}, formatter);
//=>  "Fred, who is 25 years old."

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:

Lens s a -> (a -> a) -> s -> s
Lens s a = Functor f => (a -> f a) -> s -> f s

We start with the type alias, Lens s a = Functor f ? (a → f a) → s → f s. This tells us that the type Lens is parameterized by two generic variables, s, and a. We know that there is a constraint on the type of the f variable used in a Lens: it must be a Functor. With that in mind, we see


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...