Name | # | Haskell | Ramda | Sanctuary | Signature |
---|---|---|---|---|---|
identity | I | id |
identity |
I |
a → a |
constant | K | const |
always |
K |
a → b → a |
apply | A | ($) |
call |
I ¹ |
(a → b) → a → b |
thrush | T | (&) |
applyTo |
T |
a → (a → b) → b |
duplication | W | join ² |
unnest ² |
join ² |
(a → a → b) → a → b |
flip | C | flip |
flip |
flip |
(a → b → c) → b → a → c |
compose | B | (.) , fmap ² |
map ² |
compose , map ² |
(b → c) → (a → b) → a → c |
substitution | S | (<*>) ² |
ap ² |
ap ² |
(a → b → c) → (a → b) → a → c |
chain | S_³ | (=<<) ² |
chain ² |
chain ² |
(a → b → c) → (b → a) → b → c |
converge | S2³ | apply2way , liftA2 ², liftM2 ² |
lift2 ² |
(b → c → d) → (a → b) → (a → c) → a → d |
|
psi | P | on |
on |
on |
(b → b → c) → (a → b) → a → a → c |
fix-point⁴ | Y | fix |
(a → a) → a |
¹) The A-combinator can be implemented as an alias of the I-combinator. Its implementation in Haskell exists because the infix nature gives it some utility. Its implementation in Ramda exists because it is overloaded with additional functionality.
²) Algebras like ap
have different implementations for different types.
They work like Function combinators only for Function inputs.
³) I could not find a consistent name for these combinators, but they are common enough in the JavaScript ecosystem to justify their inclusion. I named them myself in order to refer to their implementation.
⁴) In JavaScript and other non-lazy languages, it is impossible to
implement the Y-combinator. Instead a variant known as the applicative or
strict fix-point combinator is implemented. This variant is sometimes
rererred to as the Z-combinator. The implementation found in combinators.js
is the strictly evaluated "Z" combinator, which needs the extra wrapper
around g (g)
on the right hand side.
@dotnetCarpenter I'll attempt to make it as clear yet short as I can.
Scott encodings are a way to represent arbitrary data types that are made up of different cases (e.g. case
True
vs caseFalse
, or caseEmptyList
vs caseList of head and tail
) each of which may contain some sub-data (e.g.List of head and tail
).Booleans
The Scott encoding for booleans is the same as the Church encoding: namely, a bool is a function that takes two arguments (what to do if the bool is True and what to do if the bool is False):
We have two values in our datatype (True and False) so we have two functions (one represents each value); and each of those functions is used by supplying two arguments (what to do in each case). We could then use it like this:
We take our boolean variable
darkMode
and we feed it what to do in each case (the case wheredarkMode
isTrue
and the case wheredarkMode
isFalse
). If the former, our result is"black"
, and if the latter, our result is"white"
.Types with 3 values
Booleans have two values. What if we have a datatype with three values – say, Rock | Paper | Scissors?
There are three functions (one for each value), and each takes three arguments (what to do in each case) and uses just one of them (e.g. the Paper function returns what the user wants to do if the value is Paper – since the Paper function "knows" it is indeed Paper).
We could use it like this:
Socratic question for you: if
gameSign
is the functionScissors
, what willaction
be set to?Pairs / 2-tuples
The above shows how to write Scott encodings for types with multiple different cases (the value is Rock or Paper or Scissors). But Scott encodings also handle when a type contains multiple values at once (the value contains a Bool and a GameSign). One such example type is
Pair
, aka a 2-tuple.Here,
Pair
is a constructor which takes two arguments (the values to wrap / contain) and returns the resulting encoding of the pair (the functionaccess => access(x)(y)
. We could use the constructorPair
to make some pairs like this:And elsewhere we could use the encoded pairs by supplying a function which will be fed access to each of the wrapped values:
Socratic question for you: how would you grab the second value of each pair?
Types with both cases and wrapped values
We've seen examples of Scott encodings for one of several possibilities (Bool, RPS) and an example of a Scott constructor / encoding for a value that contains multiple values. What if we have a type that combines both of these features? A value might be A or B, and if it's B, it contains sub-value X.
An example of such a type is
Maybe
, akaOption
orOptional
in some languages:We have two cases (either a value is
Nothing
orSome x
) so we have two functions, and the encoding takes two arguments (what to do if the value is Nothing / what to do if it's Some). But theSome
encoding has to be constructed by feeding a value to wrap over (x
). And to access that value, we need to feed a function as an argument, whichSome
will passx
into so the user can use it.Example use case:
If
loggedInUser
isNothing
, then we use a default value; but if it'sSome("Naomi")
, we feed a function to get access to the wrapped name, which we can then use.Recursive Types
Now we finally come to the example of a singly-linked list. Our list encoding will be like
Maybe
above in that it has two cases: the empty list, or a list cell with a value (the "head") AND with a continuation of the list (the "tail"). Since we have two cases, we'll have two functions. Since the full list wraps two values, our constructor will take two arguments.We can use the
Cell
construcror like this:And then elsewhere, you could consume the
shoppingList
encoding like this:Challenge question: can you figure out how to use the Y-combinator to get the length of
shoppingList
?Conclusion
I just finished writing this and truth be told I am dissatisfied with it. It's much longer and less clear than I hoped. But I'm posting it because I can't spend more time right this second trying to make it better, and maybe despite those deficiencies, it will still help you make progress in your understanding. I hope so at any rate!