Skip to content

Instantly share code, notes, and snippets.

Last active April 24, 2023 22:31
What would you like to do?
Common combinators in JavaScript
const I = x => x
const K = x => y => x
const A = f => x => f (x)
const T = x => f => f (x)
const W = f => x => f (x) (x)
const C = f => y => x => f (x) (y)
const B = f => g => x => f (g (x))
const S = f => g => x => f (x) (g (x))
const S_ = f => g => x => f (g (x)) (x)
const S2 = f => g => h => x => f (g (x)) (h (x))
const P = f => g => x => y => f (g (x)) (g (y))
const Y = f => (g => g (g)) (g => f (x => g (g) (x)))
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-point4 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.

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

Note that when I use the word "combinator" in this context, it implies "function combinator in the untyped lambda calculus".

Copy link

JamieDixon commented Nov 22, 2020

@CrossEye Should I leave this earth having achieved nothing more than contributing the name fooComb to the collective parlance of combinatorial logic, I shall consider my time here well spent.

Copy link

converge is not well-known in Haskell as apply2way. It is very well-known as liftA2 or liftM2.

Copy link

Avaq commented Dec 23, 2020

Right, thanks for the heads-up @JohanWiltink. Noted.

Copy link

S_ is also known in JS as "flatMap"
I'm making a paper / repo combination detailing some of the things you can do with these, and also including the appropriate Smullyan bird (if they exist) in them.

Copy link

CrossEye commented Dec 25, 2020

@JohanWiltink: Yes I always suggest people use Ramda's lift in preference to converge whenever they can. converge has some additional capabilities for JS's variadic functions, but unless they're used, lift is nicer.

Copy link

Avaq commented Dec 28, 2020

@CrossEye I believe I still can't quite list R.lift as an implementation of (b → c → d) → (a → b) → (a → c) → a → d, because R.lift takes its first function argument in strictly uncurried form. As in, I think R.lift could be an implementation of ((b, c) → d) → (a → b) → (a → c) → a → d instead.

Copy link

CrossEye commented Dec 29, 2020

@Avaq: sorry, I wasn't suggesting that, only responding to something @JohanWiltink said.

And yes, it's not an implementation of that pattern, although the first argument can in fact be a curried function. The next functions, though, cannot be supplied separately. So you can do

lift ((a) => (b) => a + b) (x => x.a, x => x.b) ({a: 2, b: 3})  //=> 5

But not

lift ((a) => (b) => a + b) (x => x.a) (x => x.b) ({a: 2, b: 3}) //=> <nonsense>

So it's more like

(b  c  d)  ((a  b), (a  c))  a  d

(although as usual with Ramda functions, the first argument can be uncurried or curried.)

Copy link

Avaq commented Dec 29, 2020

I wasn't suggesting that [R.lift is a valid implementation of the converge combinator], only responding to something

I know, but when I saw your comment I flew in to add R.lift to the table, but then started to question whether I can. I commented just to verify. Sorry, it was a little unclear. :)

although as usual with Ramda functions, the first argument can be uncurried or curried

Oh, interesting. I didn't know Ramda functions are also overloaded in that respect. Also, I thought R.lift determines the arity for the lifted function based on the arity of the original, but I guess it uses the length of arguments supplied to the lifted result instead.

Copy link


I commented just to verify.

You're correct. lift is related, but not an exact match, as is Ramda's converge.

Also, I thought R.lift determines the arity for the lifted function based on the arity of the original, but I guess it uses the length of arguments supplied to the lifted result instead.

Your initial thought was correct. However if those two numbers don't match, it probably won't work.

And of course, this use of lift is still a fairly obscure one. I would expect it more for cases like

lift ((a, b) => a + b) (Maybe (25), Maybe (17)) //=> Maybe (42)

Copy link

stken2050 commented Sep 28, 2021

a → b → b is also useful:

const right = a => b => b;
const log = a => right(console.log(a))(a);

This behaves like identity function: a => a which does not affect to the original code but console.log(a) in the process.

It's possible to rewrite with a → b → a

const left = a => b => a;
const log = a => left(a)(console.log(a));

but, It's not intuitive in terms of the evaluation order.

Copy link

jethrolarson commented Sep 29, 2021

@stken2050 Fortunately its trivial

const CK = C(K)

I don't see why console.log is a good case though. Maybe you meant 'tap':

const tap = (f) => (x) => {
  return x;


// Or more usefully: 

It is impure though

Copy link

Sure, log is IO and impure, and actually, I use the right for any IO operations.

Copy link

Hello! Ramda v0.28.0 has shipped with some new functions, including on: — Do you think we could update the table above?

Copy link

drupol commented Jan 25, 2022

It looks like that new on is actually the Psi combinator.

Copy link

Avaq commented Jan 26, 2022

Thanks for the heads-up. Ramda on added to the table! 🎉

Copy link

CrossEye commented Sep 8, 2022


I have this as a pinned tab and I just stare at it sometimes like the obelisk in 2001.

Six years later, and I'm still doing that!

@Avaq: Thank you for a wonderful resource!

Copy link

Avaq commented Sep 15, 2022

Thank you @CrossEye ❤️
I also still commonly refer to this resource, myself. :)

Copy link

Same as gazing avianly at the Smullyan book, which I got in ebook after buying it physically.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment