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.
I'm going to assume you meant
(f -> a -> a) -> a -> a
. You missed an extraa -> ...
for input you're giving inYFact (4)
. Under that assumption, you are almost correct!There's a little thing missing though. You specified
f
in that signature, but what isf
? You're implying it's a function by using the letterf
, but you haven't given that function a signature. What is the signature off
? In your factorial example it's thefact
function, so let's go with that. You calledfact
onn - 1
(which is a member ofa
) and it produced anothera
(we know that, because you're multiplying its result). So the signature off
is actuallya -> a
! Let's update our whole signature:Interestingly, we see a pattern arise:
(a -> a)
occurs three times! We can see that more clearly if I add the implicit braces explicitly. I'll also give the three functions some names so we can talk about them.I'll add names to your example as well, to make it easier to talk about:
The
recur
function is what you call to trigger the recursion. But recursion into what? Well, into theoutput
function. And then thefixed
function is what's returned from the whole thing.Now why did
recur
andfixed
have the shapea -> a
? It's becauseoutput
has that shape! Therecur
function just calls back intooutput
, so it has the same shape asoutput
. Andfixed
has the same shape asoutput
too, becauseY
is just returning youroutput
.You already discovered that you can change the shape of
output
with your definition ofYPlus
, and thatrecur
andfixed
just followed:That's your second example, updated to include the names. You updated
output
to take two arguments, and consequentlyrecur
andfixed
also took two arguments. TheY
function now has the following signature:Where did that third
-> a
come from?! From your ownoutput
function! Can we also reduce the number of-> a
s by not returning a function at all?Yep! 😄 In this last example, the signature of
Y
is:And that signature actually works for all the other examples too, because
a -> a
,a -> a -> a
, or any other Function or value are all members ofa
, so long as they are consistent throughout the type signature. So the least restrictive signature for theY
function is(a -> a) -> a
🎉The signature
(a -> a) -> a
basically captures the pattern we saw, that whatevera
you're returning froma -> a
(even if it's a function) is the samea
that you get as input, and the samea
that is returned as final output.