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