使用右折和差异列表对列表进行教堂编码

时间:2018-07-23 19:26:22

标签: javascript arrays list functional-programming church-encoding

这是后面的顺序问题

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

Extracting data from a function chain without arrays

在这里,我要特别感谢@Aadit M Shah和@ user633183

现在,打开这个问题来阐明Difference listChurch list之间的异同或关系


差异列表

https://stackoverflow.com/a/51320041/6440264

  

difference list是一个函数,它接受一个列表并在其前面添加另一个列表。例如:

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.

  

关于差异列表的最酷的事情是它们形成了一个monoid,因为它们只是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
  

更好的是,您可以将它们打包到一个整洁的小类中,其中函数组成是点运算符:

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
  

您可以使用apply方法来检索DList的值,如下所示:

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]

  

使用差异列表而不是常规列表(功能列表,而不是JavaScript数组)的优点在于,由于列表是从右到左连接的,因此连接效率更高。因此,如果要连接多个列表,它不会一遍又一遍地复制相同的值。


Church Encoding List

作为使用教堂对进行编码的一种替代方法,可以使用右折叠功能对列表进行识别来对列表进行编码。例如,三个元素x,y和z的列表可以由一个高阶函数编码,该函数在应用于组合器c和值n时会返回c x(c y(c z n))。

https://stackoverflow.com/a/51420884/6440264

  

user633183's solution很出色。它使用Church encoding of lists using right folds减轻了继续操作的需要,从而使代码更简单,易于理解。这是她的解决方案,经过修改以使foldr看起来像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]

  

这里g是到目前为止累积的教会编码列表。最初是空列表。调用g会将其从右侧折叠。但是,我们也从右侧构建列表。因此,由于我们编写列表的方式,似乎我们正在构建列表并将其从左侧折叠。


  

如果所有这些功能使您感到困惑,那么user633183真正在做什么:

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]

  

如您所见,她正在向后构建列表,然后使用reduceRight向后折叠向后列表。因此,看起来您正在构建列表并将其向前折叠。


我想在差异列表中看到的是

  1. 理解似乎是自然而直接的。
  2. 通过归位(展平),它会形成monoids
  3. 身份元素是身份功能,不需要提供外部初始值。

我不喜欢的东西

  1. 至少,提供的示例代码取决于JavaScript数组

事实上,我在教堂清单中喜欢/不喜欢的是上述内容的对立面。

我喜欢

  1. 它是JavaScript数组实现的独立功能,它可以自己定义操作:user633183's solution

我不喜欢

  1. 我不知道为什么它不能左折而是右折?
  

可以通过使用右折叠功能对其进行识别来对列表进行编码

  1. 不清楚Monoid的实现

  2. 尤其是,Nil不是Identity元素(=身份函数),并且示例代码需要提供外部初始值。

所以,我很好奇的是,像教堂清单这样的差异清单是否正式化了。

规格将是

  • 基本上,这是一个差异列表

  • JavaScipt数组实现的独立性

  • 初始值是内置的身份识别功能。

谢谢你

4 个答案:

答案 0 :(得分:3)

问题的根源

一系列问题背后的问题根源在于您坚持使用L(1)(2)(3)语法来构建列表。这种语法没有任何意义,人们已经一次又一次告诉您放弃使用这种语法:

  1. user633183's answer是您的第一个问题:

      

    函数currying和可变参数实际上并不能一起工作。一旦意识到以下两个表达式不兼容,这一限制就会变得显而易见

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

    L (1)之上返回一个列表,但是在第二个表达式中,我们希望L (1)是可以应用于2的函数。 L (1)可以是列表,它可以是产生列表的函数;不能同时使用。

  2. Bergi's comment关于第二个问题:

      

    首先,如果您想使用函数式编程,请避免使用可变参数函数或奇怪的混合返回类型。

  3. user633183's answer(第三个问题):

      

    所以说到类型,让我们检查一下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
    
         

    autoCons总是返回一个lambda,但是该lambda具有我们无法确定的类型-有时它返回另一个相同类型的lambda,有时返回一个完全不同的结果;在这种情况下,6

         

    因此,我们无法轻松地将autoCons表达式与程序的其他部分混合和组合。如果您丢掉这个错误的驱动器来创建可变参数的咖喱界面,则可以制作一个autoCons可输入类型的

当您只需编写L(1)(2)(3)时,我看不出有任何理由使用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);

此外,如果使用L(1)(2)(3)语法的唯一原因是“有效地”将元素推到列表的末尾,那么您也可以使用普通列表来这样做。只需向后构建列表,然后使用cons在列表的开头放置一个新元素。

列表的代数结构

您似乎对列表的结构有一些非正统的信念:

  1. 首先,you believe列表的开头应始终为nil:

      

    如Lisp / Scheme教科书中所述,构造列表的传统方法是非常错误的。 Nil不应该在列表的末尾,而应该在列表的头。 Lisp / Scheme给程序设计世界带来了如此多的困惑,它们具有扭曲的列表结构(尾部为0 =无)。

  2. 第二,you believe,您不必为列表折叠提供初始值:

      

    我仍然不知道您坚持使用“ init”值进行折叠等任何理由,看看某些库,它们不使用“ init”,我认为它们更合理。 github.com/benji6/church/blob/master/src/lists.js确切地说,他们基本上将Zero = Identity用于更有意义的初始化。

这两种信念都是不明智的。要了解为什么我们需要查看列表的代数结构:

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

列表可以为空或非空。空列表由null表示。通过使用cons将新元素放在另一个(可能为空)元素列表之前,可以形成非空列表。我们将新元素放在原始列表的前面而不是它的后面,因为它更自然:

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.

注意:使用snoc并没有本质上的错误。我们可以将List定义为List a = null | snoc (List a, a)。但是,使用cons更自然。现在,根据我们使用cons还是snoc来定义List数据类型,将新元素放在列表前面还是将新元素放在列表后面都变得很昂贵:

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

注意:在接下来的两段中使用Haskell语法。

Difference lists用于通过延迟列表的串联直到需要,然后以最有效的顺序对其进行串联,来分摊昂贵操作的成本。例如,假设我们有一个表达式as ++ bs ++ cs ++ ds,其中我们要连接四个列表。如果我们使用cons的{​​{1}}实现,那么串联的最有效顺序是List,这就是Haskell中(++)运算符是正确关联的原因。另一方面,如果我们使用as ++ (bs ++ (cs ++ ds))的{​​{1}}实现,则串联的最有效顺序是snoc

使用List的{​​{1}}实现时,差异列表的格式为((as ++ bs) ++ cs) ++ ds,其中cons是常规列表。我们可以使用常规函数组合(即List)将它们向前组合。使用(xs ++)的{​​{1}}实现时,差异列表的格式为xs,其中(as ++) . (bs ++) . (cs ++) . (ds ++)是常规列表。我们可以使用常规函数组合(即snoc)向后组合它们。这是为什么更优选使用List的{​​{1}}实现的另一个原因。

现在,让我们换一下齿轮,谈论一下非空列表的一部分。对于列表(无论我们使用的是(++ xs)的{​​{1}}实现还是xs的{​​{1}}实现),术语(++ ds) . (++ cs) . (++ bs) . (++ as)consListcons具有非常具体的含义:

List
  1. 非空列表的head是列表的第一个元素。
  2. 非空列表的tail除了列表的第一个元素外,都是其他内容。
  3. 非空列表的init是除列表的最后一个元素之外的所有内容。
  4. 非空列表的last是列表的最后一个元素。

因此,取决于我们使用snoc还是List来定义headtailinit的{​​{1}}数据类型并且last变得昂贵:

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

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

无论如何,这就是为什么声明“ Nil不应该在列表的末尾,而应该在列表的头”的原因是没有意义的。列表的开头是列表的第一个元素。 Nil不是列表的第一个元素。因此,声明nil应该始终是列表的开头是不合逻辑的。


现在,让我们继续前进。根据我们使用cons还是snoc来定义List数据类型,headtail都将成为尾递归:

init

如果该语言执行tail call optimization,则尾递归通常更为有效。但是,结构递归更natural,在具有延迟评估的语言中becomes more efficient,它可以在无限数据结构上工作。说到无限数据结构,last实现无限向前扩展(即 head / tail init / last ┌─────────────┬─────────────┐ cons │ Inexpensive │ Expensive │ ├─────────────┼─────────────┤ snoc │ Expensive │ Inexpensive │ └─────────────┴─────────────┘ ),而cons实现无限扩展向前(即snoc)。与List相比,foldl更为偏爱的另一个原因。

无论如何,让我们尝试理解为什么需要折叠的初始值。假设我们有以下列表foldr,并使用 foldl foldr ┌──────────────────────┬──────────────────────┐ cons │ Tail Recursion │ Structural Recursion │ ├──────────────────────┼──────────────────────┤ snoc │ Structural Recursion │ Tail Recursion │ └──────────────────────┴──────────────────────┘ 进行折叠:

cons

如您所见,当我们使用cons(1, cons(2, cons(3, ....)))简化列表时,我们实际上是将每个snoc替换为snoc(snoc(snoc(...., 1), 2), 3),并将cons替换为{{1 }}。这样,您可以执行以下操作:折叠第一个列表,将snoc替换为xs = cons(1, cons(2, cons(3, null))),将foldr替换为第二个列表 cons func / \ / \ 1 cons 1 func / \ -> foldr(func, init, xs) -> / \ 2 cons 2 func / \ / \ 3 null 3 init

foldr

现在,如果您不提供初始值,那么您就不会保留列表的结构。因此,您将无法追加两个列表。实际上,您甚至无法重建相同的列表。考虑:

cons

使用func可以找到列表的总和而不提供初始值(即null),但是如何在不借助witchcraft的情况下重建同一列表呢?如果您愿意提供初始值,则可以优雅地编写init。否则,除非您打破函数式编程的原理并使用副作用从cons本身内部人为地提供初始值,否则不可能这样做。无论哪种方式,无论是通过显式指定初始值还是通过处理列表的最后一个元素作为cons中的特殊情况,您都将提供一个初始值。

选择更简单的选择。明确提供初始值。正如Zen of Python所述:

  

美丽胜于丑陋。
  显式优于隐式。
  简单胜于复杂。
  ...
  特殊情况不足以违反规则。

无论如何,请转到最后一节。

您正在寻找的答案(以及更多)

如果我不回答您的任何问题,就给您讲课是不合适的。因此:

  1. 关于差异列表,您的以下说法是错误的:

      
        
    1. 身份元素是身份功能,并且不需要提供外部初始值。
    2.   

    实际上,如果折叠差异列表,则仍然需要提供初始值。作为参考,请参见Hackage上null包中的foldr函数。

  2. 关于教会编码列表,您有以下问题:

      
        
    1. 我不知道为什么它不能左折而是右折?
    2.   

    由于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 )。因此,如果要“向前”折叠列表,则必须使用 cons func / \ / \ 1 cons 1 func / \ -> foldr1(func, xs) -> / \ 2 cons 2 func / \ / 3 null 3 而不是foldr1。请注意,如果我们使用foldr1(plus, xs)而不是foldr(cons, null, xs),那么它实际上是转发的(即func)。这是由于func只是Data.DList且参数已翻转。因此,L(1)(2)(3)的{​​{1}}等同于L(1)(2)(3) = cons(3, cons(2, cons(1, null)))的{​​{1}},反之亦然,这是user633183注意到的。

    请注意,我最初使用延续的解决方案实际上确实为foldr使用了foldl,但是为了做到这一点,我不得不以某种方式将列表反向,因为它是向后构建的。这就是延续的目的,以反转列表。直到后来我才意识到我根本不需要撤消这份清单。我可以简单地使用snoc而不是cons

  3. 关于教会编码列表的第二点:

      
        
    1. 不清楚Monoid的实现
    2.   

    所有列表都是monoid,其中标识元素为L(1)(2)(3) = snoc(snoc(snoc(null, 1), 2), 3),二进制操作为snoc。请注意,cons(左侧标识)和foldr(右侧标识)。此外,cons等效于foldl(关联性)。

  4. 关于教会编码列表的第三点:

      
        
    1. 特别地,Nil不是Identity元素(=身份函数),并且示例代码需要提供外部初始值。
    2.   

    是的,nil实际上是列表的标识元素。如果snoc数据类型被实现为差异列表,则nil是身份函数。否则,这是另一回事。尽管如此,nil始终是列表的标识元素。

    我们已经讨论了为什么需要外部初始值。如果您不提供它们,那么您将无法执行某些操作,例如foldl。您必须提供初始值以附加两个列表。您可以通过显式地提供列表的第一个元素(使用cons时)或最后一个元素(使用foldr时)来特意提供初始值,也可以通过人工方式来提供初始值(因此)违反了函数式编程的原理。

  5. 最后,关于您的梦想界面:

      

    所以,我很好奇的是,像教堂清单这样的差异清单是否正式化了。

    您为什么要这样做?您希望实现什么?教堂编码仅在理论上很有趣。实际上这不是很有效。此外,差异列表仅在您随意地串联列表时才有用(从而利用差异列表的单曲面结构使它们变平)。将两者结合是一个非常糟糕的主意。

无论如何,我希望您不再提出这样的问题,并花一些时间阅读SICP

答案 1 :(得分:2)

  

我不知道为什么它不能左折而是右折?

没有诸如“一定不能向左折叠”或“必须绝对向右折叠”之类的东西。我的实现是一个选择,我为您提供了一个非常小的程序,使您有信心自行选择

  

不清楚与Monoids的关系

The implementation I gave代表append是二元半二进制运算,nil是标识元素。

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)
  

特别地,Nil不是Identity元素(=身份函数),并且示例代码需要提供外部初始值。

否,nil是如上所述的标识元素。


通常,您的问题串涉及有关在不使用JavaScript复合数据[]{}的情况下实现列表样式数据类型的各种方法。

实际上,有无数种实现列表的方法。当然,有许多常规设计,但是如果您的目标是自己创建一个,则没有“最佳”甚至“更好”的类型。每个实现都围绕一组标准进行设计。

差异列表和Church的右对齐列表只是两种可能的编码。我们可以将完全不同的编码用于简化列表–

const nil =
  () => nil

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

此列表可以向左或向右折叠

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

使用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)

请注意,即使nilcons构造了一个完全不同的数据结构,它们在本质上还是与以前的实现相同。 是数据抽象的强大要素。

lengthtoArray不需要更改。我们可以实现Monoid接口–

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)

单子?当然–

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)

是否适用?

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)

要点是,这些实现中没有一个是特别特殊的。由于nilcons形成了可靠的合同,此处的列表和我的其他答案中给出的列表可以轻松满足这些接口。与差异列表相同–这只是具有明确定义和可靠行为的另一种实现。每个实现都有自己的性能配置文件,并且在不同情况下的执行情况会有所不同。

作为练习,您应该尝试自己实现nilcons,然后从那里构建其他一阶和高阶函数。


  

如Lisp / Scheme教科书中所述,构造列表的传统方法是非常错误的。 Nil不应该在列表的末尾,而应该在列表的头。 Lisp / Scheme给程序设计世界带来了如此多的困惑,它们具有扭曲的列表结构(尾部为0 =无)。

您不知道您在说什么

答案 2 :(得分:-1)

我的实现:

身份/无头无尾

无需对初始值进行硬编码。

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 ]

基于此实现,我想回应

Church encoding of lists using right folds and difference lists

  

问题的根源

     

一系列问题背后的问题根源在于您坚持使用L(1)(2)(3)语法来构建列表。

正如我们已经确认的那样,仅通过函数构造列表没有错。教堂编码是一种使用咖喱函数构造所有事物的方式。因此,此声明无效。

  

这种语法没有任何意义,人们已经一次又一次地告诉您放弃使用这种语法:

如果您坚持认为某事没有任何意义的原因是由于“人们一次又一次地告诉您放弃”,我害怕地说您错了,让我们检查一下人们在说什么。

  
      
  1. user633183's answer第一个问题:
  2.   
     

函数currying和可变参数实际上并不能一起工作。一旦意识到以下两个表达式不兼容,这一限制就会变得显而易见

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

L (1)之上返回一个列表,但是在第二个表达式中,我们希望L (1)是可以应用于2的函数。 L (1)可以是列表,它可以是产生列表的函数;不能同时使用。

类型不匹配问题已经解决,很抱歉,L不再存在此问题。

  
      
  1. Bergi's comment关于第二个问题:
  2.   
     

首先,如果您想使用函数式编程,请避免使用可变参数函数或奇怪的混合返回类型。

同样,类型不匹配问题已经解决,很抱歉,L不再存在此问题。

  
      
  1. user633183's answer(第三个问题):
  2.   
     

所以说到类型,让我们检查一下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
  

autoCons总是返回一个lambda,但是该lambda具有我们无法确定的类型-有时它返回另一个相同类型的lambda,有时返回一个完全不同的结果;在这种情况下,6

     

因此,我们无法轻松地将autoCons表达式与程序的其他部分混合和组合。如果您丢掉这个错误的驱动器来创建可变参数的咖喱界面,则可以制作一个autoCons可输入类型的

同样,类型不匹配问题已经解决,很严重,L不再存在此问题,请注意,不是我自己实现了L的情况值,而不用L包装。

  

当您只需编写L(1)(2)(3)时,我看不出有任何理由使用toList([1,2,3])语法:

当有另一种书写方式时,也绝对没有理由禁止使用L(1)(2)(3)语法。这是一个选择问题。

  

此外,如果使用L(1)(2)(3)语法的唯一原因是“有效地”将元素推到列表的末尾,那么您也可以使用普通列表来这样做。只需向后构建列表,然后使用cons在列表的开头放置一个新元素。

稍后我必须对效率进行评论,但是到目前为止,为什么地球上有人必须在现有的方法自然而又简单地实现翻转列表的同时向后实现翻转列表?您如何证明打破简单性只是为了支持发烧使用“正常清单”? “正常”是什么意思?

不幸的是,我在这里找不到任何“问题的根源”。

  

列表的代数结构

     

您似乎对列表的结构有一些非正统的信念:

     
      
  1. 首先,you believe列表的开头应始终为nil:
  2.   
     

如Lisp / Scheme教科书中所述,构造列表的传统方法是非常错误的。 Nil不应该在列表的末尾,而应该在列表的头。 Lisp / Scheme给程序设计世界带来了如此多的困惑,它们具有扭曲的列表结构(尾部为0 =无)。

正确。实际上,我还有更多未定义的理由。我待会儿定义。

  
      
  1. 第二,you believe,您不必为列表折叠提供初始值:
  2.   
     

我仍然不知道您坚持使用“ init”值进行折叠等任何理由,看看某些库,它们不使用“ init”,我认为它们更合理。 github.com/benji6/church/blob/master/src/lists.js确切地说,他们基本上将Zero = Identity用于更有意义的初始化。

正确。

  

这两种信念都是不明智的。要了解为什么我们需要查看列表的代数结构:

     

列表可以为空或非空。空列表由null表示。通过使用cons将新元素放在另一个(可能为空)元素列表之前,可以形成非空列表。我们将新元素放在原始列表的前面而不是它的后面,因为它更自然:

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.

好吧,我现在能理解你的坚持

1 + 2 + 3很难以顺序操作的形式记为二进制运算符,因为它是

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

我们应该引入“每条尾巴都没有”,因为它更容易阅读和书写?认真地?我不同意,我想知道其他人的感受。

好吧,表达下面的结构

  

a的列表为空,或者为a和另一个a的列表的缺点。

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

const list2 = pair(list1)(2);

对我来说看起来更“自然”。实际上,该结构的语法直接对应于Append操作。

此外,cons / Nils的事实如下:

enter image description here

对于列表列表,用户/代码需要添加多个Nils,并且必须在每个cons操作上实现Nils插入检查逻辑。这确实很麻烦,并且失去了代码的简单性。

对于“ snoc”,Nil / Zero / Null / 0or1不管拳头单元的身份是什么,因此不需要对每个操作进行Nil插入检查。同样,这与我们不对每次二进制操作(例如+x)都检查Nil插入检查相同。我们只关心头部或根部的身份。

  

注意:使用snoc并没有本质上的错误。我们可以将List定义为List a = null | snoc (List a, a)。但是,使用cons更自然。现在,根据我们使用cons还是snoc来定义List数据类型,将新元素放在列表前面还是将新元素放在列表后面都变得很昂贵:

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

很明显,“落后”或“附加”成本较低。我们需要将新数据放在现有列表的前面,这很少见。

  

注意:在接下来的两段中使用Haskell语法。

     

差异列表...这是使用cons的{​​{1}}实现更为可取的另一个原因。

诸如操作成本差异之类的hack要求是“ snoc”不需要的hack。因此,我真的不理解您的意见,认为存在变通方法是有利的。

  

现在,让我们换一下齿轮,谈论一下非空列表的一部分。对于列表(无论我们使用的是List的{​​{1}}实现还是cons的{​​{1}}实现),术语ListsnocListhead具有非常具体的含义:

tail
  1. 非空列表的head是列表的第一个元素。
  2. 非空列表的tail除了列表的第一个元素外,都是其他内容。
  3. 非空列表的init是除列表的最后一个元素之外的所有内容。
  4. 非空列表的last是列表的最后一个元素。
  

因此,取决于我们使用init还是last来定义 head tail │ ┌──────────┴─────────┐ cons(1, cons(2, cons(3, null))); └──────┬─────┘ │ init last init last ┌──────────┴─────────┐ │ snoc(snoc(snoc(null, 1), 2), 3); │ └─┬─┘ head tail conssnoc的{​​{1}}数据类型并且List变得昂贵:

head

这是正确的,在通常情况下,代码需要一个新数据=“ last”和累积数据=“ init”,并且在我自己的代码中实现起来如此容易,因为“ snoc” / tail以便宜的价格为init(“ init”)和last(“ last”)提供了

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

它非常简洁,易于实现,读/写和理解。

当然,简单性来自pair二进制运算符和left(“ snoc”)的顺序操作之间的相同结构。

right
  

无论如何,这就是为什么声明“ Nil不应该在列表的末尾,而应该在列表的头”的原因是没有意义的。列表的开头是列表的第一个元素。 Nil不是列表的第一个元素。因此,声明nil应该始终是列表的开头是不合逻辑的。

我没有任何理由选择更复杂的结构,特别是对于初学者。

实际上const plus = a => b => Number(a) + Number(b); const sum = left => right => left === I ? right : plus(left(sum))(right); log( list3(sum) ); 这个词到处都有,而Plus却很少见。

https://en.wikipedia.org/wiki/Cons 甚至没有描述pair的小词,当然也没有解释。我认为这确实是不健康的情况。这是怎么回事?

我知道这里有一个历史背景:https://en.wikipedia.org/wiki/S-expression,并且很重要的一点是,尊重先锋作品是很重要的,但是,高估简单结构的复杂性只能由威权主义来解释。

我真的很抱歉,但我可能应该指出部分责任是您的,实际上,他们是经验丰富的程序员和热情的导师,像你们一样出于某种原因高估了//`1 + 2 + 3` plus(plus(plus(0, 1), 2), 3); snoc(snoc(snoc(ID, 1), 2), 3); 而又低估了{{1} }。

如果我是一位老师,要教孩子们做单子,首先要介绍哪种结构? “ Snoc”。 它向前发展,更易于理解和使用。

类似于顺序二进制运算。

cons

容易。

缺点?与Nils在一起。


我会将其余的Respnse分隔到另一篇文章中,因为这太长了。=>

https://stackoverflow.com/a/51510563/6440264

答案 3 :(得分:-1)

这是https://stackoverflow.com/a/51500775/6440264的续集

回复@ Aadit M Shah

  

现在,让我们继续前进。根据我们使用cons还是snoc来定义List数据类型,foldlfoldr都将成为尾递归:

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

如果该语言执行tail call optimization,则尾递归通常更为有效。但是,结构递归更natural,在具有延迟评估的语言中becomes more efficient,它可以在无限数据结构上工作。说到无限数据结构,cons实现无限向前扩展(即cons(1, cons(2, cons(3, ....)))),而snoc实现无限扩展向前(即snoc(snoc(snoc(...., 1), 2), 3))。与cons相比,snoc更为偏爱的另一个原因。

作为结构递归的snoc的折叠是natural,我想在那里分享答案。 https://stackoverflow.com/a/32276670/6440264

  

“自然”(或只是“结构”)递归是开始教学生有关递归的最佳方法。这是因为它具有Joshua Taylor指出的绝妙保证:它保证终止。学生们花了足够的时间来学习这类程序,使之成为“规则”可以为他们节省大量的防墙撞墙费用。

     

当您选择退出结构递归的领域时,您(程序员)承担了另外的责任,即确保您的程序在所有输入上都停止运行;思考和证明是另一回事。

还有另一个原因是喜欢snoc而不是cons。

  

无论如何,让我们尝试理解为什么需要折叠的初始值。假设我们有以下列表xs = cons(1, cons(2, cons(3, null))),并使用foldr进行折叠:

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

如您所见,当我们使用foldr简化列表时,我们实际上是将每个cons替换为func,并将null替换为{{1 }}。这样,您可以执行以下操作:折叠第一个列表,将init替换为cons,将cons替换为第二个列表null

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
  

使用 cons func / \ / \ 1 cons 1 func / \ -> foldr1(func, xs) -> / \ 2 cons 2 func / \ / 3 null 3 可以找到列表的总和而不提供初始值(即foldr1),但是如何在不借助witchcraft的情况下重建同一列表呢?如果您愿意提供初始值,则可以优雅地编写foldr1(plus, xs)。否则,除非您打破函数式编程的原理并使用副作用从foldr(cons, null, xs)本身内部人为地提供初始值,否则不可能这样做。无论哪种方式,无论是通过显式指定初始值还是通过处理列表的最后一个元素作为func中的特殊情况,您都将提供一个初始值。

好吧,当我编写没有初始值的代码并反对这一系列“应提供初始值”的观点时,我真的不明白您为什么这么做。

我已经显示了代码的一部分,但是再次,这是一个方法:

func

当您对“初始值”进行硬编码时,您实际上在做什么?

例如,对于“加”运算,如何选择初始值应为const plus = a => b => Number(a) + Number(b); const sum = left => right => left === I ? right : plus(left(sum))(right); log( list3(sum) );

它不是从哪里来的吗?绝对不会,0实际上是由二进制运算符本身定义的初始值。

在您的脑海中,您想,“好0 + a = a = a + 0,所以它必须是初始值!”,

或者您认为,“确定1 * a = a = a * 1,所以必须是!”,

或者您认为,“好吧,[]。concat(a)= [a],所以[]就是初始值!”

对吗?你在做什么?您只需拿起头脑中的标识元素,这绝对是胡说八道,因为我们使用计算机并编写代码!

如果您真正需要的是标识元素,请编写代码。至少我做到了。

0

如果它是const sum = left => right => left === I //hit the bottom of pairs ? right // reflect the right value of the bottom pair. ,则表示底部对的正确值,因为我是身份I,实际上,我可以将代码重写为:

I = a=>a

请注意,因为它触及了最下面的一对,所以循环操作:

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

通过这种方式,我们不必浪费脑部操作就可以识别诸如(left)(right)01之类的显而易见的初始值,这些初始值基本上是身份值。 / p>

[]

可以定义二进制运算符来标识与左/右折实现无关的第一个/最后一个。

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