Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@Avaq
Last active March 18, 2024 20:49
Show Gist options
  • Save Avaq/1f0636ec5c8d6aed2e45 to your computer and use it in GitHub Desktop.
Save Avaq/1f0636ec5c8d6aed2e45 to your computer and use it in GitHub Desktop.
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-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.

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

@glebec
Copy link

glebec commented Aug 14, 2020

@kirilloid

What I don't understand is W. join in Haskell usually collapses a container so its type should be a (a b) → a b rather than (a → a → b) → a → b. Is it some kind of generalization? Or is it what footnote 2 is about?

This reply is years later, so forgive me if you're already way past this point in understanding. But: join collapses monadic contexts, not containers.

-- some type aliases for clearer alignment
type List a = [a]
type Func i o = i -> o

-- general type, then specialized to different monads
join :: Monad m => m        (m        a)   ->  m        a
join ::            List     (List     a)   ->  List     a
join ::            Maybe    (Maybe    a)   ->  Maybe    a
join ::            IO       (IO       a)   ->  IO       a
join ::            Proxy    (Proxy    a)   ->  Proxy    a
join ::            Either x (Either x a)   ->  Either x a
join ::            Func   i (Func   i a)   ->  Func   i a
join ::            (i ->    (i ->     a))  ->  (i ->    a)

The join function fuses together a single nested layer of monadic contexts. What is a monadic context for (A)? Sometimes, it's a "container"-like structure, e.g. list of (A) or either X or (A). Sometimes, however, it's not really a container at all, e.g. function with input type X and output type (A) or an IO program that would produce (A). (The ultimate non-container monad Proxy for (A), defined as data Proxy a = Proxy, which has a phantom type param and thus never holds an a-type value.)

The specific monad cited in this combinators article, corresponding to the W combinator, is the "Reader" monad – that is, the monad of functions which take a specified input type (and may have different output types).

type Reader i o = i -> o

-- not allowed to define instances for type aliases, but if you could:
instance Monad (Reader i) where
-- (implementation elided)

This type is usually cited in Haskell as ((->) r) for annoying syntactic reasons. It would be more intuitive as r -> _ but that syntax isn't allowed in class instance definitions, so we have a sectioned operator (->) followed by its first input type (r), yielding the type signature of a "function which takes type r as its input".

instance Monad ((->) i) where
-- (implementation elided)

Anyway, since the reader monad is a function with a specific input type, nested reader monads would be something like "a function of input type r, and output type (a function with input type r, and output type x). Or, to use type signatures:

exampleReader       ::       R -> X
exampleNestedReader :: R -> (R -> X)

Then, it becomes much clearer (at least, in terms of type signatures) how join for the reader monad collapses the R -> monads into a single R ->:

-- again, using `Func` as a maybe-less-confusing alias for `->`:
join :: Monad m => m      (m      a)   ->  m      a
join ::            Func i (Func i a)   ->  Func i a
join ::            (i ->  (i ->   a))  ->  (i ->  a)

Remember, the function arrow is right-associative, so (i -> (i -> a)) is the same as i -> i -> a:

join :: (i -> i -> a) -> (i -> a)

And we can again use the right-associativity of -> to simplify one step further:

join :: (i -> i -> a) -> i -> a

So there we go, join in Haskell is the same as the W combinator, when we are talking about the Reader monad. (Obviously join will not be the same as W when join is specialized to any other monad, e.g. List or Maybe or IO. That's what the footnote was saying.) The thing we are collapsing, the monadic context, is the input side of a function arrow: r -> (written in Haskell typeclass instance sigs as ((->) r)).

You may want to know, what on earth do you use the reader monad for? This is out of scope, but… the short answer is that functions which all take the same input-type can be chained together so they all receive a single value as that input. That value then becomes a dynamic / parameterized shared readable constant – a configuration value which each function can read. So in practice, the reader monad is used in Haskell to make it easy to thread read-only config data through an application.

Properly explaining and demonstrating the reader monad in practice is a whole topic unto itself, so I recommend anyone interested consult tutorials/articles/docs/books/videos on the subject. I just wanted to focus on the following major points:

  • Not all monads are containers
  • The W combinator is the same as join for the reader monad (but not join for other monads)
  • The reader monad is "functions of input type r" (for some r type variable)

@drupol
Copy link

drupol commented Nov 5, 2020

Hey,

I'm following this gist actively :-) I created a PHP project out of it, find it here: https://github.com/loophp/combinator

I will add the new combinators tonight!

@JamieDixon
Copy link

Hey all,

I love this list and come back to it regularly.

I recently wrote a combinator and I'm trying to find out the name of it. I'll do my best to describe it here and perhaps someone who's knowledgeable in these things will know what it's called.

The function signature I've identified is: (a -> b -> c) -> (a -> c -> d) -> (a -> b -> d) which is a function that takes 2 binary functions. When a value is applied to the resulting function, that value is applied to both of the binary functions. When the next value is applied it is then applied to the second function in the composition, the result of which is applied to the first function in the composition.

It's a little bit like substitution but for 2 binary functions instead of 1 unary and 1 binary.

My implementation of this (using Crocks) looks like: const fooComb = converge(binary(compose));

Thanks again for maintaining this excellent list and I hope my explanation above is expressed clearly enough.

@davidchambers
Copy link

As far as I'm aware, @JamieDixon, that combinator does not have a name.

When a value is applied to the resulting function, that value is applied to both of the binary functions.

Functions are applied to arguments, not the other way around. f (x) is f applied to x. I know what you meant, of course. :)

@JamieDixon
Copy link

Thanks @davidchambers

You are of course completely right, that functions are applied to arguments. I'm not sure why I wrote it that way around!

I'm excited to find out that I've produced a combinator without a name. Perhaps I shall name it 🙂

@danwdart
Copy link

danwdart commented Nov 19, 2020

As far as your (a -> b -> c) -> (a -> c -> d) -> (a -> b -> d), could you provide an instance? It sounds a little like some things I've seen before and might be able to be abstracted. For instance, it seems like a flipped compose with an extra argument.

@drupol
Copy link

drupol commented Nov 19, 2020

Hi,

I'm also curious to see an instance of that.

@JamieDixon
Copy link

JamieDixon commented Nov 19, 2020

@danwdart @drupol Let me know if I've misunderstood what you're asking for, but here's how I make use of this function, including some example functions for my app:

const filterBodyProps = id => body => ...
const parseObject = id => obj => ...

// The combinator that is thus-far unnamed
// fooComb is now: (a -> b -> c) -> (a -> c -> d) -> (a -> b -> d)
const fooComb = converge(binary(compose));

const filterAndParseFields = fooComb(parseObject, filterBodyProps);

const result = filterAndParseFields("abc-123")({ name: "bob", price: "500"})

I've been wondering whether the first two functions should be swapped around to bring this more inline with the standard thinking around composition.

@danwdart
Copy link

Reminds me of the Reader monad with composition.

@JamieDixon
Copy link

@danwdart Yes! That's a great way to express it. In this case the id is the shared context and the rest is standard function composition.

@drupol
Copy link

drupol commented Nov 19, 2020

For some reason it's easier for me to use the JS notation:

const FooComb  = f => g => x => y => g(x)(f(x)(y))

Is it good @JamieDixon ?

@JamieDixon
Copy link

@drupol Looks good to me!

@CrossEye
Copy link

@JamieDixon: I came here wondering if the combinator I was about to write had a well-known name.

I guess not. But now I think FooComb may be stuck in my head!

@JamieDixon
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.

@JohanWiltink
Copy link

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

@Avaq
Copy link
Author

Avaq commented Dec 23, 2020

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

@danwdart
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.

@CrossEye
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.

@Avaq
Copy link
Author

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.

@CrossEye
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.)

@Avaq
Copy link
Author

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.

@CrossEye
Copy link

@Avaq:

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)

@ken-okabe
Copy link

ken-okabe 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.

@jethrolarson
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) => {
  f(x);
  return x;
};

tap(console.log)(2)

// Or more usefully: 
doThing().then(tap(console.log))

It is impure though

@ken-okabe
Copy link

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

@customcommander
Copy link

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

@drupol
Copy link

drupol commented Jan 25, 2022

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

@Avaq
Copy link
Author

Avaq commented Jan 26, 2022

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

@CrossEye
Copy link

CrossEye commented Sep 8, 2022

@JAForbes:

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!

@Avaq
Copy link
Author

Avaq commented Sep 15, 2022

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

@danwdart
Copy link

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

@dotnetCarpenter
Copy link

I was scratching my head about the Y-combinator and how to use it. But after reading up on the fix-point combinator, it dawned on me.

It's a way to implement recursion in an anonymous function.

Example:

const Y = f => (g => g (g)) (g => f (x => g (g) (x)))

// create a recursive function in an anonymous function
const YFact = Y (fact => n => n === 0 ? 1 : n * fact (n - 1))

YFact (4) // <- 24

// If `fact` was a named function, `YFact` would be equal to the following:
const fact = n => n === 0 ? 1 : n * fact (n - 1)

The above code snippet shows two implementation of a function that calculates the factorial of an integer. YFact is made out of an anonymous function, which can not reference itself without the Y-combinator.

@dotnetCarpenter
Copy link

dotnetCarpenter commented Dec 29, 2023

@Avaq Given my comment above, shouldn't the signature for fix-point be (f -> a -> a) -> a?
ehh.. no, on the other hand. The resulting function from the Y-combinator is not limited to monadic functions. It could have any number of parameters.

Example, with an anonymous dyadic function:

const YPlus = Y (plus => n => m => n === 0 ? m : 1 + plus (n - 1) (m))

YPlus (2) (2)  // <-  4

// If `plus` was a named function, `YPlus` would be equal to the following:
const plus = n => m => n === 0 ? m : 1 + plus (n - 1) (m)

I don't know how to write variadic functions in Church encoding.

@glebec
Copy link

glebec commented Dec 29, 2023

I don't know how to write variadic functions in Church encoding.

@dotnetCarpenter in the lambda calculus there are no such things are variadic function (or technically even n-adic functions; there are only unary functions, with n-adic functions being practically emulated via currying).

That being said you can emulate variadic functions by taking a list of arguments as your single argument. Lists can be implemented in the lambda calculus via a Scott encoding of a cons list (singly-linked list):

// constructors =>               list encoding
//                /-------------------------------------------\
//     head tail
Cons = x => xs => handleCons => handleNil => handleCons (x) (xs)
Nil  =            handleCons => handleNil => handleNil
//                \_____________________/    \________________/
//                case handling functions         result

@dotnetCarpenter
Copy link

dotnetCarpenter commented Dec 29, 2023

thanks @glebec but I fail to see how Scott encoding signatures, are written from your example.

Perhaps:

Cons :: a -> [a] -> (x_1 ... x_n)
Y :: (x_1 ... x_n -> a) -> a

Scott encoding

Note

I found the following quote from Ben Lynn, on Scott encoding. Though, he does not make the distinction clear, what is Scott encoding vs Church.
"... we stress again pure lambda calculus numerals are not condemned to be unary!"
https://crypto.stanford.edu/~blynn/compiler/scott.html

The best example I can come up with, is this for encoding a list:

data [a] = [] | a : [a]
[]       = \f _ -> f
(:) a as = \_ g -> g a as

@glebec
Copy link

glebec commented Dec 29, 2023

The best example I can come up with, is this for encoding a list:

If you look closely, your example is the same as mine above (just with the trivial difference of swapping the case-handling functions). Nil or [] is a constructor which represents the empty list, and its interface is \f _ -> f or as I had written handleCons => handleNil => handleNil (again, we chose a different order for the case handling functions, but that doesn't matter so long as the usage is consistent throughout your program). Cons or (:) is the list cell constructor, and it takes a first element and a tail of elements to construct a list with the interface \_ g -> g a s or handleCons => handleNil => handleCons(x)(xs) as I had written. Again, same thing modulo case-handling order.

EDIT: I just noticed that you were quoting Ben Lynn. If you need help understanding it, I'm happy to write more later – just let me know.

@Avaq
Copy link
Author

Avaq commented Dec 29, 2023

Shouldn't the signature for fix-point be (f -> a -> a) -> a? -- @dotnetCarpenter

I'm going to assume you meant (f -> a -> a) -> a -> a. You missed an extra a -> ... for input you're giving in YFact (4). Under that assumption, you are almost correct!

There's a little thing missing though. You specified f in that signature, but what is f? You're implying it's a function by using the letter f, but you haven't given that function a signature. What is the signature of f? In your factorial example it's the fact function, so let's go with that. You called fact on n - 1 (which is a member of a) and it produced another a (we know that, because you're multiplying its result). So the signature of f is actually a -> a! Let's update our whole signature:

Y :: ((a -> a) -> a -> a) -> a -> a
--     \    /                \  /
--      \  /                  and this is the function input you forgot originally
--       that's the expansion of `f`

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.

Y :: ((a -> a) -> (a -> a)) -> (a -> a)
--     \    /      \    /       \    /
--      recur      output        fixed

I'll add names to your example as well, to make it easier to talk about:

const fixed = Y (recur => function output(n){ return n === 0 ? 1 : n * recur (n - 1) });

The recur function is what you call to trigger the recursion. But recursion into what? Well, into the output function. And then the fixed function is what's returned from the whole thing.

Now why did recur and fixed have the shape a -> a? It's because output has that shape! The recur function just calls back into output, so it has the same shape as output. And fixed has the same shape as output too, because Y is just returning your output.

You already discovered that you can change the shape of output with your definition of YPlus, and that recur and fixed just followed:

const fixed = Y (recur => function output(n){ return m => n === 0 ? m : 1 + recur (n - 1) (m) })

That's your second example, updated to include the names. You updated output to take two arguments, and consequently recur and fixed also took two arguments. The Y function now has the following signature:

Y :: ((a -> a -> a) -> (a -> a -> a)) -> (a -> a -> a)
--     \         /      \         /       \         /
--        recur            output            fixed

Where did that third -> a come from?! From your own output function! Can we also reduce the number of -> as by not returning a function at all?

Y (recur => ({output: 42}))
// -> {output: 42}

Yep! 😄 In this last example, the signature of Y is:

Y :: (a -> a) -> a

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 of a, so long as they are consistent throughout the type signature. So the least restrictive signature for the Y function is (a -> a) -> a 🎉

The signature (a -> a) -> a basically captures the pattern we saw, that whatever a you're returning from a -> a (even if it's a function) is the same a that you get as input, and the same a that is returned as final output.

@dotnetCarpenter
Copy link

Thanks @Avaq for, as usual, a beautiful example!

I guess Scott encoding, is the succinct way to describe your detailed examples. But I think your explanation is more beautiful non the less.

Big thanks to @glebec for explaining how a linked list implementation correspond to Scott encoding of the same list.

You both have mad ascii skillz

@glebec
Copy link

glebec commented Dec 29, 2023

@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 case False, or case EmptyList vs case List 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):

//          case arguments
//      /-------------------\
True  = trueCase => falseCase => trueCase
False = trueCase => falseCase => falseCase
//      \________________________________/
//              boolean encoding

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:

// one of the following is active, but we don't know which:
// darkMode = True
// darkMode = False

backgroundColor = darkMode("black")("white") // using the `darkMode` bool

We take our boolean variable darkMode and we feed it what to do in each case (the case where darkMode is True and the case where darkMode is False). 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?

//                    case arguments
//         /----------------------------------\
Rock     = rockCase => paperCase => scissorCase => rockCase
Paper    = rockCase => paperCase => scissorCase => paperCase
Scissors = rockCase => paperCase => scissorCase => scissorCase
//         \_________________________________________________/
//                        RPS encoding

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:

action = gameSign("slam")("wrap")("cut")

Socratic question for you: if gameSign is the function Scissors, what will action 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.

//  wrapped vals (constructor args)
//     /----\
Pair = x => y => access => access(x)(y)
//               \____________________/
//                    pair encoding
//    \------------------------------/
//          constructor

Here, Pair is a constructor which takes two arguments (the values to wrap / contain) and returns the resulting encoding of the pair (the function access => access(x)(y). We could use the constructor Pair to make some pairs like this:

numPair = Pair(5)(9)
strPair = Pair("hi")("bye")

And elsewhere we could use the encoded pairs by supplying a function which will be fed access to each of the wrapped values:

firstNum = numPair(x => y => x) // 5
firstStr = strPair(x => y => x) // "hi"

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, aka Option or Optional in some languages:

//                      encoding for Nothing | Some
//             /------------------------------------------\
Nothing =      handleNothing => handleSome => handleNothing
Some    = x => handleNothing => handleSome => handleSome(x)
//       \________________________________________________/
//                    constructor for Some

We have two cases (either a value is Nothing or Some 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 the Some 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, which Some will pass x into so the user can use it.

Example use case:

// loggedInUser is one of these:
// loggedInUser = Nothing
// loggedInUser = Some("Naomi")

message = loggedInUser("Nobody is logged in")(name => `Welcome, ${name}!`)

If loggedInUser is Nothing, then we use a default value; but if it's Some("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.

//                               encoding for Empty | Cell
//                      /-------------------------------------------------\
Empty =                 handleEmpty => handleCell => handleEmpty
Cell  = head => tail => handleEmpty => handleCell => handleCell(head)(tail)
//      \_________________________________________________________________/
//                     constructor for Cell

We can use the Cell construcror like this:

shoppingList = Cell("potato")(Cell("milk")(Cell("chicken")(Empty)))

And then elsewhere, you could consume the shoppingList encoding like this:

secondItem =
  shoppingList("There is no item1")(item1 => more => more("There is no item2")(item 2 => more => item2))

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!

@dotnetCarpenter
Copy link

dotnetCarpenter commented Dec 30, 2023

@glebec don't be dissatisfied - you have given me plenty to work through and its great fun!
So far I have only worked with the Bool adt. I'm posting it here, so you can comment if I have misunderstood. I plan to go through the other types later.

//                case arguments
//            /-------------------\
const True  = trueCase => falseCase => trueCase
const False = trueCase => falseCase => falseCase
//            \________________________________/
//                     boolean encoding
// Scott encoding:
// Bool  = True | False
// True  = \x _ -> x
// False = \_ y -> y
var darkMode = False
var backgroundColor = darkMode ("black") ("white") // using the `darkMode` bool
console.debug (darkMode,        // <- Function: False
               backgroundColor) // <- white

var darkMode = True
var backgroundColor = darkMode ("black") ("white") // using the `darkMode` bool
console.debug (darkMode,        // <- Function: True
               backgroundColor) // <- black

// wrapped values (constructor args)
//           /----\
const Pair = x => y => access => access(x) (y)
//                     \_____________________/
//                          pair encoding
//           \-------------------------------/
//                     constructor
// Scott encoding:
// Pair x y = Fst | Snd
// Fst      = \x _ -> x
// Snd      = \_ y -> y
const numPair = Pair (5)    (9)
const strPair = Pair ("hi") ("bye")
var   fst     = x => _ => x
var   snd     = _ => y => y
console.debug (numPair (snd), // <- 9
               strPair (snd)) // <- bye

// Pair can also be derived from Bool
// Scott encoding:
// Pair x y    = Bool = True | False
// True  = Fst = \x _ -> x
// False = Snd = \_ y -> y
var fst        = True
var snd        = False
console.debug (numPair (snd), // <- 9
               strPair (snd)) // <- bye

@dotnetCarpenter
Copy link

@glebec I can see that my Scott encoding of Pair is wrong. But what should it be?

Pair x y = x y (\x _ -> x) | x y (\_ y -> y)?

@JohanWiltink
Copy link

JohanWiltink commented Dec 30, 2023

Pair x y = \ fn . fn x y

functionally equivalent to Pair x y fn = fn x y, but trying to express that a pair is a ( binary ) function that accepts a ( binary ) function and applies that function to the values contained in the closure / pair.

The Scott encoding is Pair x y = Pair x y, which seems unhelpful. But note that there is only one, binary, constructor, which is entirely different from Boolean, where there are two nullary constructors.

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