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.
- 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
.
- 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
.
- 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:
- 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.
- 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:

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
- The
head
of a non-empty list is the first element of the list.
- The
tail
of a non-empty list is everything but the first element of the list.
- The
init
of a non-empty list is everything but the last element of the list.
- 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://stackoverflow.com/a/51510563/6440264