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://stackoverflow.com/a/51320041/6440264
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 theDList
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://stackoverflow.com/a/51420884/6440264
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 likefoldl
:
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. Callingg
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
- It seems natural and straightforwad to understand.
- With concattation (flatten), it forms monoids
- Identity element is identity function, and no need for external initial values provided.
What I don't like
- 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
- It's indpendent of JavaScript Array implementation, and it can define operations by itself : user633183's solution
I dislike
- 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
Unclear for the realation to Monoids
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.
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
The implementation I gave for
append
is the monoid binary operation,nil
is the identity element.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 –
This list can be folded left-wise or right-wise
Common map and filter functions implemented trivially with
foldlr
Notice how these are essentially identical to the previous implementation even though
nil
andcons
construct an entirely different data structure. This is the power essence of data abstraction.length
andtoArray
need no change. We can implement Monoid interface –Monad? Sure –
Applicative?
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
andcons
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
andcons
Then build the other first-order and higher-order functions from there.You have no idea what you're saying
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:user633183's answer to your very first question:
Bergi's comment on your second question:
user633183's answer to your third question:
I don't see any good reason to use the
L(1)(2)(3)
syntax when you could simply writetoList([1,2,3])
: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 usecons
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:
First, you believe that the head of the list should always be nil:
Second, you believe that you should not have to provide an initial value for list folds:
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 usingcons
. We put the new element in front of the original list instead of behind it because it's more natural:Note: There's nothing inherently wrong with using
snoc
. We could defineList
asList a = null | snoc (List a, a)
. However, it's just more natural to usecons
. Now, depending upon whether we usecons
orsnoc
to define theList
data type, either putting new elements in front of a list or putting new elements behind a list becomes expensive: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 thecons
implementation ofList
then the most efficient order of concatenation isas ++ (bs ++ (cs ++ ds))
, which is why the(++)
operator in Haskell is right associative. On the other hand, if we're using thesnoc
implementation ofList
then the most efficient order of concatenation is((as ++ bs) ++ cs) ++ ds
.When using the
cons
implementation ofList
, a difference list has the form(xs ++)
wherexs
is a regular list. We can compose them forwards using regular function composition (i.e.(as ++) . (bs ++) . (cs ++) . (ds ++)
). When using thesnoc
implementation ofList
, a difference list has the form(++ xs)
wherexs
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 thecons
implementation ofList
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 ofList
or thesnoc
implementation ofList
), the termshead
,tail
,init
andlast
have very specific meanings:head
of a non-empty list is the first element of the list.tail
of a non-empty list is everything but the first element of the list.init
of a non-empty list is everything but the last element of the list.last
of a non-empty list is, well, the last element of the list.Hence, depending upon whether we use
cons
orsnoc
to define theList
data type, eitherhead
andtail
orinit
andlast
becomes expensive: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
orsnoc
to define theList
data type, eitherfoldl
orfoldr
becomes tail recursive: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 thesnoc
implementation grows infinitely backwards (i.e.snoc(snoc(snoc(...., 1), 2), 3)
). Yet another reason to prefercons
oversnoc
.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 usingfoldr
:As you can see, when we reduce a list using
foldr
we're essentially replacing everycons
withfunc
and we're replacingnull
withinit
. This allows you to do things like append two lists by folding the first list, replacingcons
withcons
andnull
with the second list,ys = 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:
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 writefoldr(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 withinfunc
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 withinfunc
.Choose the simpler alternative. Explicitly provide an initial value. As the Zen of Python states:
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:
With respect to difference lists, your following statement is wrong:
Actually, if you fold a difference list then you still need to provide an initial value. For reference, see the
foldr
function from theData.DList
package on Hackage.With respect to Church encoded lists, you had the following question:
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 usefoldr
instead offoldl
. Note that if we usesnoc
instead ofcons
then it's actually forwards (i.e.L(1)(2)(3) = snoc(snoc(snoc(null, 1), 2), 3)
). This follows from the fact thatsnoc
is justcons
with the arguments flipped. Therefore,foldr
forcons
is equivalent tofoldl
forsnoc
and vice versa, which is what user633183 noticed.Note that my initial solution using continuations did in fact use
foldl
forcons
, 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 usefoldr
instead offoldl
.With respect to your second point about Church encoded lists:
All lists are monoids, where the identity element is
null
and the binary operation isappend = (xs, ys) => foldr(cons, ys, xs)
. Note thatfoldr(cons, null, xs) = xs
(left identity) andfoldr(cons, ys, null) = ys
(right identity). Furthermore,foldr(cons, zs, foldr(cons, ys, xs))
is equivalent tofoldr(cons, foldr(cons, zs, ys), xs)
(associativity).With respect to your third point about Church encoded lists:
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 usingfoldl
) or last element (when usingfoldr
) of the list as a special case (and thereby breaking the principles of functional programming).Finally, with respect to your dream interface:
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.
This is a sequel to https://stackoverflow.com/a/51500775/6440264
Response to @ Aadit M Shah
The foldl of snoc as Structural Recursion is natural , and I would like to share the answer there. https://stackoverflow.com/a/32276670/6440264
Yet another reason to prefer snoc over cons.
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:
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.
If it is
I
reflect the right value of the bottom pair, because I is an identityI = a=>a
, and in fact, I could rewrite the code as: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
or1
or[]
that are fundamentally identity values.It is possible to define binary operators to identify first/last that is independent of left/right fold implementation.
My implementation:
Identity/Nil on the head not tails
No hard-coding initial values required.
Based on this implementation, I would like to respond to
Church encoding of lists using right folds and difference lists
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.
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.
The type mismatch issue is already resolved, hense, this problem does not exist any more for
L
.Again, the type mismatch issue is already resolved, hense, this problem does not exist any more for
L
.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 makeL
returns a naked value, without wrapped byL
.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.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.
Correct. In fact, I have some more reason that I have not defined yet. I will define later.
Correct.
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'sand 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
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
+
orx
. We only care for the identity on the head or root.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.
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.
head
of a non-empty list is the first element of the list.tail
of a non-empty list is everything but the first element of the list.init
of a non-empty list is everything but the last element of the list.last
of a non-empty list is, well, the last element of the list.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 theleft
("init") andright
("last") with inexpensive cost.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 andpair
("snoc").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 underestimatesnoc
.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.
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