Church encoding of lists using right folds and difference lists
Asked Answered
J

4

0

Here is the sequential question after

How to store data of a functional chain of Monoidal List?

and

Extracting data from a function chain without arrays

and here I would like to express my respect and appreciation for contributors to my Questions, especially by @Aadit M Shah and @user633183

Now, this question is opened to clarify the similarities and differences or relation between Difference list and Church list

.


Difference list

https://mcmap.net/q/1781695/-how-to-store-data-of-a-functional-chain-of-monoidal-list

A difference list is a function that takes a list and prepends another list to it. For example:

const concat = xs => ys => xs.concat(ys); // This creates a difference list.

const f = concat([1,2,3]); // This is a difference list.

console.log(f([])); // You can get its value by applying it to the empty array.

console.log(f([4,5,6])); // You can also apply it to any other array.

The cool thing about difference lists are that they form a monoid because they are just endofunctions:

const id = x => x; // The identity element is just the id function.

const compose = (f, g) => x => f(g(x)); // The binary operation is composition.

compose(id, f) = f = compose(f, id);                   // identity law
compose(compose(f, g), h) = compose(f, compose(g, h)); // associativity law

Even better, you can package them into a neat little class where function composition is the dot operator:

class DList {
    constructor(f) {
        this.f  = f;
        this.id = this;
    }

    cons(x) {
        return new DList(ys => this.f([x].concat(ys)));
    }

    concat(xs) {
        return new DList(ys => this.f(xs.concat(ys)));
    }

    apply(xs) {
        return this.f(xs);
    }
}

const id = new DList(x => x);

const cons = x => new DList(ys => [x].concat(ys));   // Construct DList from value.

const concat = xs => new DList(ys => xs.concat(ys)); // Construct DList from array.

id . concat([1, 2, 3]) = concat([1, 2, 3]) = concat([1, 2, 3]) . id // identity law

concat([1, 2]) . cons(3) = cons(1) . concat([2, 3]) // associativity law

You can use the apply method to retrieve the value of the DList as follows:

class DList {
    constructor(f) {
        this.f  = f;
        this.id = this;
    }

    cons(x) {
        return new DList(ys => this.f([x].concat(ys)));
    }

    concat(xs) {
        return new DList(ys => this.f(xs.concat(ys)));
    }

    apply(xs) {
        return this.f(xs);
    }
}

const id = new DList(x => x);

const cons = x => new DList(ys => [x].concat(ys));

const concat = xs => new DList(ys => xs.concat(ys));

const identityLeft  = id . concat([1, 2, 3]);
const identityRight = concat([1, 2, 3]) . id;

const associativityLeft  = concat([1, 2]) . cons(3);
const associativityRight = cons(1) . concat([2, 3]);

console.log(identityLeft.apply([]));  // [1,2,3]
console.log(identityRight.apply([])); // [1,2,3]

console.log(associativityLeft.apply([]));  // [1,2,3]
console.log(associativityRight.apply([])); // [1,2,3]

An advantage of using difference lists over regular lists (functional lists, not JavaScript arrays) is that concatenation is more efficient because the lists are concatenated from right to left. Hence, it doesn't copy the same values over and over again if you're concatenating multiple lists.


Church Encoding List

As an alternative to the encoding using Church pairs, a list can be encoded by identifying it with its right fold function. For example, a list of three elements x, y and z can be encoded by a higher-order function that when applied to a combinator c and a value n returns c x (c y (c z n)).

https://mcmap.net/q/1782035/-extracting-data-from-a-function-chain-without-arrays

user633183's solution is brilliant. It uses the Church encoding of lists using right folds to alleviate the need for continuations, resulting in simpler code which is easy to understand. Here's her solution, modified to make foldr seem like foldl:

const L = g => function (x, a) {
    switch (arguments.length) {
    case 1: return L((f, a) => f(g(f, a), x));
    case 2: return g(x, a);
    }
};

const A = L((f, a) => a);

const xs = A(1)(2)(3)(4)(5);

console.log(xs((x, y) => x + y, 0));        // 15
console.log(xs((x, y) => x * y, 1));        // 120
console.log(xs((a, x) => a.concat(x), [])); // [1,2,3,4,5]

Here g is the Church encoded list accumulated so far. Initially, it's the empty list. Calling g folds it from the right. However, we also build the list from the right. Hence, it seems like we're building the list and folding it from the left because of the way we write it.


If all these functions are confusing you, what user633183 is really doing is:

const L = g => function (x, a) {
    switch (arguments.length) {
    case 1: return L([x].concat(g));
    case 2: return g.reduceRight(x, a);
    }
};

const A = L([]);

const xs = A(1)(2)(3)(4)(5);

console.log(xs((x, y) => x + y, 0));        // 15
console.log(xs((x, y) => x * y, 1));        // 120
console.log(xs((a, x) => a.concat(x), [])); // [1,2,3,4,5]

As you can see, she is building the list backwards and then using reduceRight to fold the backwards list backwards. Hence, it looks like you're building and folding the list forwards.


What I like to see in Diffrence List is

  1. It seems natural and straightforwad to understand.
  2. With concattation (flatten), it forms monoids
  3. Identity element is identity function, and no need for external initial values provided.

What I don't like

  1. At least, the sample code provided depends on JavaScript Array

What I like/dislike in Church List is, in fact, the opoosite of the above.

I like

  1. It's indpendent of JavaScript Array implementation, and it can define operations by itself : user633183's solution

I dislike

  1. I don't know why it must be not left but right fold?

a list can be encoded by identifying it with its right fold function

  1. Unclear for the realation to Monoids

  2. Especailly, Nil is not Identity element( = identity function), and the sample code need for external initial values provided.

So, what I am curious is is there any formalization of Diffrence list like Church-list.

The specificaton would be

  • Basically, it's a difference list

  • Indenpendent of JavaScipt Array implementation

  • The initial value is the built-in identety function.

Thsnk you.

Jugum answered 23/7, 2018 at 19:26 Comment(0)
G
4

The Root of the Problem

The root of the problem underlying your series of questions is your insistence on using the L(1)(2)(3) syntax to construct a list. This syntax just doesn't make any sense, and people have told you time and again to forgo using this syntax:

  1. user633183's answer to your very first question:

    Function currying and variadic arguments don't really work together. It's a restriction made obvious once you realize that the following two expressions are incompatible

    L (1)     -> [ 1 ]
    L (1) (2) -> [ 1, 2 ]
    

    Above L (1) returns a list, but in the second expression we expect L (1) to be a function that we can apply to 2. L (1) can be a list or it can be a function that produces a list; it cannot be both at the same time.

  2. Bergi's comment on your second question:

    First of all, if you want to embrace functional programming, avoid variadic functions or curiously mixed return types.

  3. user633183's answer to your third question:

    So speaking of types, let's examine the type of autoCons

    autoCons (1)                  // "lambda (x,n) => isFunction (x) ...
    autoCons (1) (2)              // "lambda (x,n) => isFunction (x) ...
    autoCons (1) (2) (3)          // "lambda (x,n) => isFunction (x) ...
    autoCons (1) (2) (3) (add, 0) // 6
    

    Well autoCons always returns a lambda, but that lambda has a type that we cannot determine – sometimes it returns another lambda of its same kind, other times it returns a completely different result; in this case some number, 6

    Because of this, we cannot easily mix and combine autoCons expressions with other parts of our program. If you drop this perverse drive to create variadic curried interfaces, you can make an autoCons that is type-able

I don't see any good reason to use the L(1)(2)(3) syntax when you could simply write toList([1,2,3]):

// null :: List a
// cons :: (a, List a) -> List a
const cons = (head, tail) => ({ head, tail });

// xs :: List Number
const xs = cons(1, cons(2, cons(3, null))); // You can either construct a list manually,

// toList :: Array a -> List a
const toList = array => array.length ? cons(array[0], toList(array.slice(1))) : null;

// ys :: List Number
const ys = toList([1,2,3]); // or you can construct a list from an array.

console.log(xs);
console.log(ys);

Furthermore, if your only reason to use the L(1)(2)(3) syntax is to “efficiently” push an element to the end of the list, then you can do so with normal lists too. Just build the list backwards and use cons to put a new element at the beginning of the list.

The Algebraic Structure of Lists

You seem to have some unorthodox beliefs about the structure of lists:

  1. First, you believe that the head of the list should always be nil:

    the traditional way to construct list as explained in lisp/Scheme textbook is very wrong. Nil should not be in the tail of the list, instead it should be in the head. Lisp/Scheme brought so much confusion having twisted list structure(0 =nil in the tail) to programming world.

  2. Second, you believe that you should not have to provide an initial value for list folds:

    I still don't know any justification you stick to use "init" value for fold etc, Looking at some libraries, they don't use "init", and I think they are more reasonable. github.com/benji6/church/blob/master/src/lists.js To be precise, they basically use Zero=Identity for init that makes more sense.

Both of these beliefs are ill-informed. To understand why we need to look at the algebraic structure of lists:

   ┌──────────────────────────── A List of a
   │   ┌──────────────────────── is
   |   |   ┌──────────────────── either null
   |   |   |  ┌───────────────── or
   |   |   |  |   ┌───────────── cons of
   |   |   |  |   |   ┌───────── an a and
   │   |   |  |   |   |     ┌─── another List of a.
┌──┴─┐ │ ┌─┴┐ | ┌─┴┐  |  ┌──┴─┐
List a = null | cons (a, List a)

A list can either be empty or non-empty. Empty lists are represented by null. Non-empty lists are formed by putting a new element in front of another (possibly empty) list of elements by using cons. We put the new element in front of the original list instead of behind it because it's more natural:

cons(1, cons(2, cons(3, null))); // This is easier to read and write.

snoc(snoc(snoc(null, 1), 2), 3); // This is more difficult to read and write.

Note: There's nothing inherently wrong with using snoc. We could define List as List a = null | snoc (List a, a). However, it's just more natural to use cons. Now, depending upon whether we use cons or snoc to define the List data type, either putting new elements in front of a list or putting new elements behind a list becomes expensive:

       in front of     behind
     ┌─────────────┬─────────────┐
cons │ Inexpensive │  Expensive  │
     ├─────────────┼─────────────┤
snoc │  Expensive  │ Inexpensive │
     └─────────────┴─────────────┘

Note: Using Haskell syntax for the next two paragraphs.

Difference lists are used to amortize the cost of the expensive operation by delaying the concatenation of lists until required and then concatenating them in the most efficient order. For example, suppose we have the expression as ++ bs ++ cs ++ ds where we are concatenating four lists. If we're using the cons implementation of List then the most efficient order of concatenation is as ++ (bs ++ (cs ++ ds)), which is why the (++) operator in Haskell is right associative. On the other hand, if we're using the snoc implementation of List then the most efficient order of concatenation is ((as ++ bs) ++ cs) ++ ds.

When using the cons implementation of List, a difference list has the form (xs ++) where xs is a regular list. We can compose them forwards using regular function composition (i.e. (as ++) . (bs ++) . (cs ++) . (ds ++)). When using the snoc implementation of List, a difference list has the form (++ xs) where xs is a regular list. We can compose them backwards using regular function composition (i.e. (++ ds) . (++ cs) . (++ bs) . (++ as)). This is another reason why using the cons implementation of List is more preferable.

Now, let's change gears and talk about parts of a non-empty list. When it comes to lists (regardless of whether we're using the cons implementation of List or the snoc implementation of List), the terms head, tail, init and last have very specific meanings:

   head          tail
     │  ┌──────────┴─────────┐
cons(1, cons(2, cons(3, null)));
└──────┬─────┘       │
     init          last

              init         last
     ┌──────────┴─────────┐  │
snoc(snoc(snoc(null, 1), 2), 3);
                     │   └─┬─┘
                   head  tail
  1. The head of a non-empty list is the first element of the list.
  2. The tail of a non-empty list is everything but the first element of the list.
  3. The init of a non-empty list is everything but the last element of the list.
  4. The last of a non-empty list is, well, the last element of the list.

Hence, depending upon whether we use cons or snoc to define the List data type, either head and tail or init and last becomes expensive:

       head / tail   init / last
     ┌─────────────┬─────────────┐
cons │ Inexpensive │  Expensive  │
     ├─────────────┼─────────────┤
snoc │  Expensive  │ Inexpensive │
     └─────────────┴─────────────┘

Anyway, this is the reason why the statement, “Nil should not be in the tail of the list, instead it should be in the head,” makes no sense. The head of the list is the first element of the list. Nil is not the first element of the list. Therefore, it's illogical to state that nil should always be the head of the list.


Now, let's move onto folds. Depending upon whether we use cons or snoc to define the List data type, either foldl or foldr becomes tail recursive:

               foldl                  foldr
     ┌──────────────────────┬──────────────────────┐
cons │    Tail Recursion    │ Structural Recursion │
     ├──────────────────────┼──────────────────────┤
snoc │ Structural Recursion │    Tail Recursion    │
     └──────────────────────┴──────────────────────┘

Tail recursion is usually more efficient if the language performs tail call optimization. However, structural recursion is more natural, and in languages with lazy evaluation it becomes more efficient and it can work on infinite data structures. Speaking of infinite data structures, the cons implementation grows infinitely forwards (i.e. cons(1, cons(2, cons(3, ....)))) whereas the snoc implementation grows infinitely backwards (i.e. snoc(snoc(snoc(...., 1), 2), 3)). Yet another reason to prefer cons over snoc.

Anyway, let's try to understand why the initial value of a fold is required. Suppose we have the following list, xs = cons(1, cons(2, cons(3, null))) and we fold it using foldr:

  cons                                         func
 /    \                                       /    \
1    cons                                    1    func
    /    \      -> foldr(func, init, xs) ->      /    \
   2    cons                                    2    func
       /    \                                       /    \
      3    null                                    3    init

As you can see, when we reduce a list using foldr we're essentially replacing every cons with func and we're replacing null with init. This allows you to do things like append two lists by folding the first list, replacing cons with cons and null with the second list, ys = cons(4, cons(5, cons(6, null))):

  cons                                       cons
 /    \                                     /    \
1    cons                                  1    cons
    /    \      -> foldr(cons, ys, xs) ->      /    \
   2    cons                                  2    cons
       /    \                                     /    \
      3    null                                  3    cons
                                                     /    \
                                                    4    cons
                                                        /    \
                                                       5    cons
                                                           /    \
                                                          6    null

Now, if you don't provide an initial value then you aren't preserving the structure of the list. Hence, you won't be able to append two lists. In fact, you won't even be able to reconstruct the same list. Consider:

  cons                                    func
 /    \                                  /    \
1    cons                               1    func
    /    \      -> foldr1(func, xs) ->      /    \
   2    cons                               2    func
       /    \                                  /
      3    null                               3

Using foldr1 you can find the sum of the list without providing an initial value (i.e. foldr1(plus, xs)), but how would you reconstruct the same list without resorting to witchcraft? If you're willing to provide an initial value then you can elegantly write foldr(cons, null, xs). Otherwise, it's impossible to do so unless you break the principles of functional programming and use side effects to artificially provide an initial value from within func itself. Either way, you are going to provide an initial value whether it's by explicitly specifying an initial value or by handling the last element of the list as a special case within func.

Choose the simpler alternative. Explicitly provide an initial value. As the Zen of Python states:

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
...
Special cases aren't special enough to break the rules.

Anyway, moving on to the final section.

The answers you were looking for (and more)

It would be improper of me to lecture you without answering any of your questions. Hence:

  1. With respect to difference lists, your following statement is wrong:

    1. Identity element is identity function, and no need for external initial values provided.

    Actually, if you fold a difference list then you still need to provide an initial value. For reference, see the foldr function from the Data.DList package on Hackage.

  2. With respect to Church encoded lists, you had the following question:

    1. I don't know why it must be not left but right fold?

    Because of your wonky L(1)(2)(3) syntax, you can only build the list backwards (i.e. L(1)(2)(3) = cons(3, cons(2, cons(1, null)))). Hence, if you want to fold the list “forwards” then you have to use foldr instead of foldl. Note that if we use snoc instead of cons then it's actually forwards (i.e. L(1)(2)(3) = snoc(snoc(snoc(null, 1), 2), 3)). This follows from the fact that snoc is just cons with the arguments flipped. Therefore, foldr for cons is equivalent to foldl for snoc and vice versa, which is what user633183 noticed.

    Note that my initial solution using continuations did in fact use foldl for cons, but in order to do that I had to somehow reverse the list since it was being built backwards. That's what the continuations were for, to reverse the list. It only later occurred to me that I don't need to reverse the list at all. I could simply use foldr instead of foldl.

  3. With respect to your second point about Church encoded lists:

    1. Unclear for the realation to Monoids

    All lists are monoids, where the identity element is null and the binary operation is append = (xs, ys) => foldr(cons, ys, xs). Note that foldr(cons, null, xs) = xs (left identity) and foldr(cons, ys, null) = ys (right identity). Furthermore, foldr(cons, zs, foldr(cons, ys, xs)) is equivalent to foldr(cons, foldr(cons, zs, ys), xs) (associativity).

  4. With respect to your third point about Church encoded lists:

    1. Especailly, Nil is not Identity element( = identity function), and the sample code need for external initial values provided.

    Yes, nil is in fact the identity element for lists. If the List data type is implemented as a difference list then nil is the identity function. Otherwise, it's something else. Nevertheless, nil is always the identity element for lists.

    We've already discussed why external initial values are necessary. If you don't provide them, then you can't do certain operations like append. You have to provide the initial value to append two lists. Either you provide the initial value explicitly or you provide the initial value artificially by handling the first element (when using foldl) or last element (when using foldr) of the list as a special case (and thereby breaking the principles of functional programming).

  5. Finally, with respect to your dream interface:

    So, what I am curious is is there any formalization of Diffrence list like Church-list.

    Why would you want to do that? What do you hope to achieve? Church encoding is only interesting in theory. It's not very efficient in practice. Furthermore, difference lists are only useful when you're concatenating lists haphazardly (thereby taking advantage of the monoidal structure of difference lists to flatten them). Combining the two is a really bad idea.

Anyway, I hope you stop asking such questions and take some time to read SICP.

Gottfried answered 24/7, 2018 at 18:26 Comment(5)
"Church encoding is only interesting in theory" I agree with this; study of Church encoding will make your head explode wrt the various ways things can be abstracted with lambda. The abstraction skills you learn will definitely help you when writing functional programs, but using Church encoding in a real program is not practical. Imagine if Javascript only gave you vars and lambda; beginners would be lost would be lost before "Hello world".Dovetail
And I totally forgot to mention how beautiful this post is. It might seem like all of this effort is falling upon deaf ears, but this kind of comprehensive answer teaches all sorts of lessons to future readers all on its own. 👏, Aadit.Dovetail
@user633183 Well, for Church encoding, I also have had the same feeling, and that is why I never learnt seriously, however, the fact is, to solve this problem, I found I do need to understand the principle. Providing the more non-abstract approach to a beginner in terms of the educational process is a completely different story, I think.Jugum
Aadit M Shah, Thank you very much for this comprehensive post. I will respond to this by editing the existing answer for the sample implementation. I have to mention for "the algebraic structure of lists" and the cost of for operations, etc.Jugum
Aadit M Shah, I partially responded. I will post another for the rest. Thanks.Jugum
D
2

I don't know why it must be not left but right fold?

There is no such thing as "it must be not left fold" or "it must be right fold". My implementation was a choice and I provided you with a very small program to give you the confidence to make choices on your own

Unclear for the relation to Monoids

The implementation I gave for append is the monoid binary operation, nil is the identity element.

const nil =
  (c, n) => n

const cons = (x, y = nil) =>
  (c, n) => c (y (c, n), x)

const append = (l1, l2) =>
  (c, n) => l2 (c, l1 (c, n))

// monoid left/right identity
append (nil, l) == l
append (l, nil) == l

// associativity
append (a, append (b, c)) == append (append (a, b), c)

Especailly, Nil is not Identity element( = identity function), and the sample code need for external initial values provided.

No, nil is the identity element as shown above.


Your string of questions seems to, in general, be about various ways to implement a list-style data type without using JavaScript compound data [] or {}.

In reality, there are countless ways to implement a list. There are many conventional designs of course, but if your goal is to create one on your own, there is not a "best" or even "better" type. Each implementation is designed around a set of criteria.

Difference lists and Church's right-fold lists are just two possible encodings. We can use an entirely different encoding for a simplified list –

const nil =
  () => nil

const cons = (x, y = nil) =>
  k => k (x, y)

This list can be folded left-wise or right-wise

const foldl = (f, init) => l =>
  l === nil
    ? init
    : l ((x, y) => foldl (f, f (init, x)) (y))

const foldr = (f, init) => l =>
  l === nil
    ? init
    : l ((x, y) => f (foldr (f, init) (y), x))

Common map and filter functions implemented trivially with foldlr

const map = f =>
  foldr
    ( (acc, x) => cons (f (x), acc)
    , nil
    )

const filter = f =>
  foldr
    ( (acc, x) =>  f (x) ? cons (x, acc) : acc
    , nil
    )

map (x => x * x) (autoCons (3, 4, 5))
// == autoCons (9, 16, 25)

filter (x => x !== 4) (autoCons (3, 4, 5))
// == autoCons (3, 5)

Notice how these are essentially identical to the previous implementation even though nil and cons construct an entirely different data structure. This is the power essence of data abstraction.

length and toArray need no change. We can implement Monoid interface –

const append = (l1, l2) =>
  l1 === nil
    ? l2
    : l1 ((x, y) => cons (x, append (y, l2)))

// left/right identity
append (nil, l) == l
append (l, nil) == l

// associativity
append (a, append (b, c)) == append (append (a, b), c)

append (autoCons (1, 2, 3), autoCons (4, 5, 6))
// == autoCons (1, 2, 3, 4, 5, 6)

Monad? Sure –

const unit = x =>
  cons (x, nil)

const bind = f =>
  foldl
    ( (acc, x) => append (acc, f (x))
    , nil
    )

// left/right identities
bind (f) (unit (x)) == f (x)
bind (unit, m) == m

// associativity
bind (g) (bind (f) (m)) == bind (x => bind (g) (f (x)))

bind (x => autoCons (x, x, x)) (autoCons (1, 2, 3))
// == autoCons (1, 1, 1, 2, 2, 2, 3, 3, 3)

Applicative?

const ap = mx =>
  bind (f => map (f) (mx))

ap (autoCons (2, 3, 4)) (autoCons (x => x * x, x => x ** x))
// == autoCons (2 * 2, 3 * 3, 4 * 4, 2 ** 2, 3 ** 3, 4 ** 4)
// == autoCons (4, 9, 16, 4, 27, 256)

The point is that not one of these implementations is particularly special. The list here and the list given in my other answer can easily satisfy these interfaces because nil and cons form a reliable contract. Same with the difference list – it's just another implementation with a well-defined and reliable behavior. Each implementation has its own performance profile and will perform differently in varying situations.

As an exercise, you should try your own implementation of nil and cons Then build the other first-order and higher-order functions from there.


the traditional way to construct list as explained in lisp/Scheme textbook is very wrong. Nil should not be in the tail of the list, instead it should be in the head. Lisp/Scheme brought so much confusion having twisted list structure(0 =nil in the tail) to programming world.

You have no idea what you're saying 😂

Dovetail answered 24/7, 2018 at 4:36 Comment(24)
Thanks to you, finally, what I'm seeking is so-called church encoding :) Surely I have heard and read Church things, but never dig deep since it can be used for list. I somehow felt or discovered by myself it's really possible to express list by pure function and didn't know it's a Church encoding. You sometimes point out this trial a golfy variadic curried interface/ or type mismatched, but I think it is formalized and reduced to (6) not 6 avoid the problem.Jugum
Anyway, thanks for your continuous educational contribution, and I posted my own answer just for reference to this topic that I must read with your answer.Jugum
I'm happy to help! In JavaScript (6) === 6 // true so I don't understand what you mean. The point I was making there is that the variadic curried function has a mixed return type – sometimes it returns another autoCons lambda, other times it returns the result of folding the list, where the type is determined by the value returned by the folding function. This mixed type means we cannot use the variadic curried autoCons with equational reasoning.Dovetail
In fact, yes. I have been stalked by malicious people in some SNS, so I had to erase the trace of my study/ questions. Excuse me for the confusion.Jugum
Repost:: I mean returns as A(6) for the reduction result, not the naked value of 6, actually, it's not me to implement to extract the naked value, and from the first I thought the returned result of the reduction also should be a list.Jugum
Church encoding is awesome. I have a few more posts about it, this one I think you will like :DDovetail
I will read your another article thankfully. In fact, I am surprised to see Monads also can be simply implemented by the Church. amazing.Jugum
I still don't know any justification you stick to use "init" value for fold etc, Looking at some libraries, they don't use "init", and I think they are more reasonable. github.com/benji6/church/blob/master/src/lists.js To be precise, they basically use Zero=Identity for init that makes more sense.Jugum
@bayesian-study Actually, even in the example you provided they do take an initial value.Gottfried
@AaditMShah I know, and it's const zero = _ => x => x not 0 but yeah, they may use one for multiply. Anyway, I think I need to implement my own list from pair. I may ask you smart guys for code review later.Jugum
@bayesian-study over the last 2 weeks you've made many sharp comments later to take back your words. Still, many of us are here to have your discussions. I encourage you to slow down a little and double, triple, quadruple-check your understandings before making concrete assertions, especially standing in the shoes of the asker.Dovetail
@bayesian-study and showing disrespect to the historical pioneers in our field is not the impression you want to be making here...Dovetail
@user633183 Thanks for your advice. I've just implemented my own, and now probably understand what I was trying to explain. Again, although I respect the historical pioneers, they are not always correct. For instance, Pie should have been 6.28... not 3.14. For list, Nil/Zero/Id should be head and not for the tails. I implemented in that way, and absolutely no hard-code initial values. Check out my new answer.Jugum
https://mcmap.net/q/1782000/-church-encoding-of-lists-using-right-folds-and-difference-lists Identity/Nil on the head not tails No hard-coding initial values required.Jugum
Sure you can fold without init but only if you can guarantee that the list is non-empty. And that means the reducer's return type is constrained to the type matching the list elements, or foldl1 : ((a, a) -> a) -> List a -> a is much less useful than foldl : ((r, a) -> r, r) -> List a -> r which can produce values of a new type – Anyway, this all begs the question: what are you really trying to do? ...Dovetail
So you have L which requires at least 1 element of type a and then can be folded by a (a, a) -> a function, producing an a value. And all the things have to be curried. And it has to be a monad, or something. What's next? What's the design goal? Do you know where you're going with this? You quickly disregard others yet boldly ask question after question...Dovetail
@user633183 re: foldl1/foldr1 and the symmetric type of its reducer function c, Richard Bird in one of his recent books proposes foldrn c fn [x] = fn x ; foldrn c fn (x:xs) = c x (foldrn c fn xs) (or something to that effect) to get the asymmetric type back. --- re: your append, I'd expect it to be (l1, l2) => (c, n) => l1 (c, l2 (c, n)), for the right fold..? --- re: JS, I've a new slogan for it in addition to "DIY heaven", after all this: it's a Common Lisp on steroids. :)Acquirement
@user633183 of course, as you might remember, I don't know JS, so I'll take your word for it (the append, I mean).Acquirement
@WillNess this topic has unfortunately been spread across about 10 questions, but append does actually work – And yeah, JS is off the rails. I've all but stopped using it as a platform to discuss and demonstrate functional concepts. Thanks for the comment as always :DDovetail
@WillNess can you link Richard Bird's foldrn if there's anything linkable?Dovetail
@WillNess So, foldrn c fn just replaces all the cons with c and replaces the last element of the list (let's call it x) with fn x. In short it handles the last element of the list as a special case. Makes sense. That's exactly what I said you'd need to do in my own answer to this question.Gottfried
@user633183 unfortunately, no; I saw it in the book itself, Pearls of Functional Algorithm Design. the "look inside" can be searched for "foldrn" (the definition is on pg.42).Acquirement
@AaditMShah yeah, so it's one of those things that get "discovered", not "invented".Acquirement
@user633183 it works because you're flipping the order of the elements in your toArray for some reason ( see xs123 here). Maybe you needed it done that way for that autoCons thing (I didn't even try to understand all of that stuff... :) ).Acquirement
J
-1

My implementation:

Identity/Nil on the head not tails

No hard-coding initial values required.

const log = (m) => {
    console.log(m); //IO
    return m;
};

const I = x => x;
const K = x => y => x;
const V = x => y => z => z(x)(y);

const left = K;
const right = K(I);

log("left right test---------");
log(
    left("boy")("girl")
);
log(
    right("boy")("girl")
);

const pair = V;

const thePair = pair("boy")("girl");

log("pair test---------");
log(
    thePair(left)
);//boy
log(
    thePair(right)
);//girl

const list1 = pair(I)(1);// Identity/Nil on the head not tails...

const list2 = pair(list1)(2);

const list3 = pair(list2)(3);

log("list right test---------");
log(
    list3(right)
);//3

//Dive into the list and investigate the behevior
log("inspect---------");
const inspect = left => right => left === I
    ? (() => {
        log(right);
        return I;
    })()
    : (() => {
        log(right);
        return left(inspect);
    })();

list3(inspect);

log("plus---------");
const plus = a => b => Number(a) + Number(b);

const sum = left => right => left === I
    ? right
    : plus(left(sum))(right);

log(
    list3(sum)
);

log("fold---------");
const fold = f => left => right => left === I
    ? right //if root Identity, reflect the right of the pair
    : f(left(fold(f)))(right);

log(
    list3(fold(plus))
);

log("list constructor---------");
const isFunction = f => (typeof f === 'function');

const _L = x => y => z => isFunction(z)
    ? L(pair(x)(y)(z)) // not a naked return value but a list
    : _L(pair(x)(y))(z);

const L = _L(I);

log(
    L(1)(2)(3)(fold(plus))
);//fold returns a list // type match

log("inspect the result------------------------");

const plusStr = a => b => String(a) + String(b);
// binary operators define the type or 
//the category of Monoid List
const unit = (a) => [a];
const join = ([a]) => [].concat(a);
const flatUnit = a => join(unit(a));
const toArray = a => x => flatUnit(a)
    .concat(x);


L(1)(2)(3)
    (fold(plus))
    (inspect);
//6

L(1)(2)(3)
    (fold(plusStr))
    (inspect);
//"123"

L(1)(2)(3)
    (fold(toArray))
    (inspect);
//[ 1, 2, 3 ]

Based on this implementation, I would like to respond to

Church encoding of lists using right folds and difference lists

The Root of the Problem

The root of the problem underlying your series of questions is your insistence on using the L(1)(2)(3) syntax to construct a list.

As we already confirmed, there is nothing wrong with to constuct a list by only functions. Church encoding is a manner to construct everything with curried function. So this statement is invalid.

This syntax just doesn't make any sense, and people have told you time and again to forgo using this syntax:

If you insisit the reason something doesn't make any sense, is due to " people have told you time and again to forgo", I'm afrad to say you are wrong, and let's check it out what people said.

  1. user633183's answer to your very first question:

Function currying and variadic arguments don't really work together. It's a restriction made obvious once you realize that the following two expressions are incompatible

L (1)     -> [ 1 ]
L (1) (2) -> [ 1, 2 ]

Above L (1) returns a list, but in the second expression we expect L (1) to be a function that we can apply to 2. L (1) can be a list or it can be a function that produces a list; it cannot be both at the same time.

The type mismatch issue is already resolved, hense, this problem does not exist any more for L.

  1. Bergi's comment on your second question:

First of all, if you want to embrace functional programming, avoid variadic functions or curiously mixed return types.

Again, the type mismatch issue is already resolved, hense, this problem does not exist any more for L.

  1. user633183's answer to your third question:

So speaking of types, let's examine the type of autoCons

autoCons (1)                  // "lambda (x,n) => isFunction (x) ...
autoCons (1) (2)              // "lambda (x,n) => isFunction (x) ...
autoCons (1) (2) (3)          // "lambda (x,n) => isFunction (x) ...
autoCons (1) (2) (3) (add, 0) // 6

Well autoCons always returns a lambda, but that lambda has a type that we cannot determine – sometimes it returns another lambda of its same kind, other times it returns a completely different result; in this case some number, 6

Because of this, we cannot easily mix and combine autoCons expressions with other parts of our program. If you drop this perverse drive to create variadic curried interfaces, you can make an autoCons that is type-able

Again, the type mismatch issue is already resolved, hense, this problem does not exist any more for L, and fianlly, please note it's Not me who have implemented to make L returns a naked value, without wrapped by L.

I don't see any good reason to use the L(1)(2)(3) syntax when you could simply write toList([1,2,3]):

There is also absolutely no reason to prohibit to use L(1)(2)(3) syntax when there is another way to write. It is a problem of choice.

Furthermore, if your only reason to use the L(1)(2)(3) syntax is to “efficiently” push an element to the end of the list, then you can do so with normal lists too. Just build the list backwards and use cons to put a new element at the beginning of the list.

I must comment for the efficiency later, but so far, why on earth someone have to implement a flip list backwards when an exisiting method alreday achieves it naturally and simply?? How can you justify that to have broken the simplicity just to support to the fever use to "normal lists"?? What do you mean by "normal"?

Unfortunately, I cannnot find any of "The Root of the Problem" here.

The Algebraic Structure of Lists

You seem to have some unorthodox beliefs about the structure of lists:

  1. First, you believe that the head of the list should always be nil:

the traditional way to construct list as explained in lisp/Scheme textbook is very wrong. Nil should not be in the tail of the list, instead it should be in the head. Lisp/Scheme brought so much confusion having twisted list structure(0 =nil in the tail) to programming world.

Correct. In fact, I have some more reason that I have not defined yet. I will define later.

  1. Second, you believe that you should not have to provide an initial value for list folds:

I still don't know any justification you stick to use "init" value for fold etc, Looking at some libraries, they don't use "init", and I think they are more reasonable. github.com/benji6/church/blob/master/src/lists.js To be precise, they basically use Zero=Identity for init that makes more sense.

Correct.

Both of these beliefs are ill-informed. To understand why we need to look at the algebraic structure of lists:

A list can either be empty or non-empty. Empty lists are represented by null. Non-empty lists are formed by putting a new element in front of another (possibly empty) list of elements by using cons. We put the new element in front of the original list instead of behind it because it's more natural:

cons(1, cons(2, cons(3, null))); // This is easier to read and write.

snoc(snoc(snoc(null, 1), 2), 3); // This is more difficult to read and write.

Well, can I understand now you insist

1 + 2 + 3 to write down as a binary operator function in sequential operation is difficult to read and write, because it's

plus(plus(plus(0, 1), 2), 3);

and we should introduce "Nil on the each tails" because it's easier to read and write? Sereiously? I would not agree, and I wonder how other people feel.

Well, to express the following strucutre

A List of a is either null or cons of an a and another List of a.

const list1 = pair(I)(1);// Identity/Nil on the head not tails...

const list2 = pair(list1)(2);

looks more "natural" to me. In fact, this syntax of the structure directly corresponds to Append operation.

Furthermore, the fact of cons/Nils is as follows:

enter image description here

For a list of lists, a user/code needs to add multiple Nils and Nils insertion check logic must be implemented on every cons operation. This is really bothersome, and lose the simplicity of the code.

For "snoc", Nil/Zero/Null/0or1 whatever is an identity elemnt of the fist unit, so no Nil insertion check is not required for each operations. Again, it's as same as we don't check Nil insertion check for each time of binary operations such as + or x. We only care for the identity on the head or root.

Note: There's nothing inherently wrong with using snoc. We could define List as List a = null | snoc (List a, a). However, it's just more natural to use cons. Now, depending upon whether we use cons or snoc to define the List data type, either putting new elements in front of a list or putting new elements behind a list becomes expensive:

       in front of     behind
     ┌─────────────┬─────────────┐
cons │ Inexpensive │  Expensive  │
     ├─────────────┼─────────────┤
snoc │  Expensive  │ Inexpensive │
     └─────────────┴─────────────┘

It's rather obvious low cost for "behind", or to append has more advantage. It's rather rare we need prepend the new data to the exisiting lists.

Note: Using Haskell syntax for the next two paragraphs.

a difference list ... This is another reason why using the cons implementation of List is more preferable.

The hack requiremet such as diffrence for operational cost is a hack that "snoc" does not need. So I really don't understand your opinion of an existence of a work-around method is advantage.

Now, let's change gears and talk about parts of a non-empty list. When it comes to lists (regardless of whether we're using the cons implementation of List or the snoc implementation of List), the terms head, tail, init and last have very specific meanings:

   head          tail
     │  ┌──────────┴─────────┐
cons(1, cons(2, cons(3, null)));
└──────┬─────┘       │
     init          last

              init         last
     ┌──────────┴─────────┐  │
snoc(snoc(snoc(null, 1), 2), 3);
                     │   └─┬─┘
                   head  tail
  1. The head of a non-empty list is the first element of the list.
  2. The tail of a non-empty list is everything but the first element of the list.
  3. The init of a non-empty list is everything but the last element of the list.
  4. The last of a non-empty list is, well, the last element of the list.

Hence, depending upon whether we use cons or snoc to define the List data type, either head and tail or init and last becomes expensive:

       head / tail   init / last
     ┌─────────────┬─────────────┐
cons │ Inexpensive │  Expensive  │
     ├─────────────┼─────────────┤
snoc │  Expensive  │ Inexpensive │
     └─────────────┴─────────────┘

Thta's right, and it's common scenario a code needs a new data = "last" and accumulated data ="init", and it has been so easy to implement in my own code because "snoc"/pair provides the left("init") and right ("last") with inexpensive cost.

const plus = a => b => Number(a) + Number(b);

const sum = left => right => left === I
    ? right
    : plus(left(sum))(right);

log(
    list3(sum)
);

It's very concise and easy to implement, read/write and understand.

Of course, the simplicity comes from identical structure between the sequencial operation of Plus binary operator and pair("snoc").

//`1 + 2 + 3` 

plus(plus(plus(0, 1), 2), 3); 

snoc(snoc(snoc(ID, 1), 2), 3);

Anyway, this is the reason why the statement, “Nil should not be in the tail of the list, instead it should be in the head,” makes no sense. The head of the list is the first element of the list. Nil is not the first element of the list. Therefore, it's illogical to state that nil should always be the head of the list.

I don't feel any reason to chose more complicated strucutre, well espcially for beginners.

In fact the word cons is used everywhere and on the other hand, snoc is very rare to find.

https://en.wikipedia.org/wiki/Cons does not describe even a signle word of snoc, and of course there is no explanation. I think this is really unhealthy situation. what is going on here??

I know there is a historical context : https://en.wikipedia.org/wiki/S-expression , and alghouth it's important to repect pinoneers works, however overestimating the complexicity over the simpler structure can only be explained by authoritarianism.

I'm really sorry but I probably should point out a part of responsiblity is yours in fact, very experienced programmers and a great mentors with enthusiasm like you guys for some reason overestimate cons and underestimate snoc.

If I was a teacher to teach list to kids, which structure comes first to introduce? "Snoc". It's straigt forward and more understandable and easier to use.

Similarity to a sequential binary operation.

//`1 + 2 + 3` 

plus(plus(plus(0, 1), 2), 3); 

snoc(snoc(snoc(ID, 1), 2), 3);

Easy.

Cons? Hard with Nils.


I will separate the rest of respnse to another post because this gets too long.=>

https://mcmap.net/q/1782000/-church-encoding-of-lists-using-right-folds-and-difference-lists

Jugum answered 24/7, 2018 at 14:9 Comment(1)
the correct interpretation of that boxes-and-pointers diagram is ((a b c) (d e f) g h i j).Acquirement
J
-1

This is a sequel to https://mcmap.net/q/1782000/-church-encoding-of-lists-using-right-folds-and-difference-lists

Response to @ Aadit M Shah

Now, let's move onto folds. Depending upon whether we use cons or snoc to define the List data type, either foldl or foldr becomes tail recursive:

               foldl                  foldr
     ┌──────────────────────┬──────────────────────┐
cons │    Tail Recursion    │ Structural Recursion │
     ├──────────────────────┼──────────────────────┤
snoc │ Structural Recursion │    Tail Recursion    │
     └──────────────────────┴──────────────────────┘

Tail recursion is usually more efficient if the language performs tail call optimization. However, structural recursion is more natural, and in languages with lazy evaluation it becomes more efficient and it can work on infinite data structures. Speaking of infinite data structures, the cons implementation grows infinitely forwards (i.e. cons(1, cons(2, cons(3, ....)))) whereas the snoc implementation grows infinitely backwards (i.e. snoc(snoc(snoc(...., 1), 2), 3)). Yet another reason to prefer cons over snoc.

The foldl of snoc as Structural Recursion is natural , and I would like to share the answer there. https://mcmap.net/q/933175/-what-is-the-definition-of-quot-natural-recursion-quot

"Natural" (or just "Structural") recursion is the best way to start teaching students about recursion. This is because it has the wonderful guarantee that Joshua Taylor points out: it's guaranteed to terminate[*]. Students have a hard enough time wrapping their heads around this kind of program that making this a "rule" can save them a huge amount of head-against-wall-banging.

When you choose to leave the realm of structural recursion, you (the programmer) have taken on an additional responsibility, which is to ensure that your program halts on all inputs; it's one more thing to think about & prove.

Yet another reason to prefer snoc over cons.

Anyway, let's try to understand why the initial value of a fold is required. Suppose we have the following list, xs = cons(1, cons(2, cons(3, null))) and we fold it using foldr:

  cons                                         func
 /    \                                       /    \
1    cons                                    1    func
    /    \      -> foldr(func, init, xs) ->      /    \
   2    cons                                    2    func
       /    \                                       /    \
      3    null                                    3    init

As you can see, when we reduce a list using foldr we're essentially replacing every cons with func and we're replacing null with init. This allows you to do things like append two lists by folding the first list, replacing cons with cons and null with the second list, ys = cons(4, cons(5, cons(6, null))):

  cons                                       cons
 /    \                                     /    \
1    cons                                  1    cons
    /    \      -> foldr(cons, ys, xs) ->      /    \
   2    cons                                  2    cons
       /    \                                     /    \
      3    null                                  3    cons
                                                     /    \
                                                    4    cons
                                                        /    \
                                                       5    cons
                                                           /    \
                                                          6    null

Now, if you don't provide an initial value then you aren't preserving the structure of the list. Hence, you won't be able to append two lists. In fact, you won't even be able to reconstruct the same list. Consider:

  cons                                    func
 /    \                                  /    \
1    cons                               1    func
    /    \      -> foldr1(func, xs) ->      /    \
   2    cons                               2    func
       /    \                                  /
      3    null                               3

Using foldr1 you can find the sum of the list without providing an initial value (i.e. foldr1(plus, xs)), but how would you reconstruct the same list without resorting to witchcraft? If you're willing to provide an initial value then you can elegantly write foldr(cons, null, xs). Otherwise, it's impossible to do so unless you break the principles of functional programming and use side effects to artificially provide an initial value from within func itself. Either way, you are going to provide an initial value whether it's by explicitly specifying an initial value or by handling the last element of the list as a special case within func.

Well, I really don't understand why you do this while I wrote a code without a initial value and disproved this series of opinion of "an initial value should be provided".

I already have shown the part of my code, but again, here's a how:

const plus = a => b => Number(a) + Number(b);

const sum = left => right => left === I
    ? right
    : plus(left(sum))(right);

log(
    list3(sum)
);

When you hard-code the "initial value", what are you really doing?

For instance, for "plus" operation, how do you chose the initial value should be 0?

Did it come from nowhere? Never, in fact 0 as the initial value is defined by the binary operator itself.

In your head, you thought, "ok 0+a = a = a + 0 , so this must be the initial value!",

or you thought, "ok 1 * a = a = a * 1, so this must be!",

or you thought, "ok, [].concat(a) = [a], so [] muse be the initial value!"

right? What are you doing? You just pick up the identity element in your head, and it's absolute nonsense to do because we use a computer and write a code!

If what you really need is identity element, code as so. At least, I did.

const sum = left => right => left === I //hit the bottom of pairs
    ? right // reflect the right value of the bottom pair.

If it is I reflect the right value of the bottom pair, because I is an identity I = a=>a, and in fact, I could rewrite the code as:

const sum = left => right => left === I
    ? (left)(right)
    : plus(left(sum))(right);

Please note since it hits the bottom pair, the loop operation:

plus(left(sum))(right) becomes (left)(right)

in this way, we don't have to waste our brain operation just to identiy the obvious initial values such as 0 or 1 or [] that are fundamentally identity values.

const list = L(3)(4)(5)

const max = (a, b) => (a > b) ? a : b;//initial value would be -Infinity
const min = (a, b) => (a < b) ? a : b;//initial value would be  Infinity

It is possible to define binary operators to identify first/last that is independent of left/right fold implementation.

const first = (a, b) => a; //initial value is 3 <= nonsense
const last = (a, b) => b; //initial value is 3 <= nonsense
// what if the list members is unknown??
Jugum answered 25/7, 2018 at 3:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.