0

这是之后的顺序问题

Monoidal List的功能链如何存储数据?

从没有数组的函数链中提取数据

在这里,我想对我的问题的贡献者表示尊重和感谢,特别是@Aadit M Shah 和@user633183

现在,这个问题被打开以澄清差异列表教会列表之间的异同或关系

.


差异列表

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

差异列表是一个函数,它接受一个列表并将另一个列表添加到它前面。例如:

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.

差异列表很酷的一点是它们形成了一个幺半群,因为它们只是内函数

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 对进行编码的替代方案,可以通过使用右折叠函数对其进行识别来对列表进行编码。例如,一个包含三个元素 x、y 和 z 的列表可以由一个高阶函数编码,该函数在应用于组合子 c 和值 n 时返回 cx (cy (czn))。

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

user633183 的解决方案非常出色。它使用Church 编码的列表使用右折叠来减轻对延续的需求,从而使代码更简单且易于理解。这是她的解决方案,修改后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是迄今为止积累的 Church 编码列表。最初,它是空列表。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. 通过连接(展平),它形成幺半群
  3. 标识元素是标识函数,不需要提供外部初始值。

我不喜欢什么

  1. 至少,提供的示例代码依赖于 JavaScript Array

事实上,我喜欢/不喜欢教堂列表的内容与上述内容相反。

我喜欢

  1. 它独立于 JavaScript Array 实现,它可以自己定义操作:user633183's solution

我不喜欢

  1. 不知道为什么一定不是左折而是右折?

可以通过使用右折叠函数识别列表来对列表进行编码

  1. 不清楚与 Monoids 的关系

  2. 特别是,Nil 不是标识元素(= 标识函数),示例代码需要提供外部初始值。

所以,我很好奇的是有没有像 Church-list 这样的 Diffrence 列表的形式化。

规格是

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

  • JavaScipt Array 实现的独立性

  • 初始值是内置的标识函数。

谢谢你。

4

4 回答 4

5

问题的根源

您的一系列问题的根本问题在于您坚持使用L(1)(2)(3)语法来构建列表。这种语法没有任何意义,人们一次又一次地告诉你不要使用这种语法:

  1. user633183对您的第一个问题的回答:

    函数柯里化和可变参数并不能真正一起工作。一旦您意识到以下两个表达式不兼容,这是一个明显的限制

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

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

  2. Bergi对您的第二个问题的评论:

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

  3. user633183对您的第三个问题的回答:

    所以说到类型,让我们来看看autoCons——</p>

    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. 首先,您认为列表的头部应该始终为 nil:

    lisp/Scheme 教科书中解释的传统构造列表的方法是非常错误的。Nil 不应该在列表的尾部,而应该在头部。Lisp/Scheme 扭曲的列表结构(尾部为 0 = nil)给编程世界带来了如此多的混乱。

  2. 其次,您认为不必为列表折叠提供初始值:

    我仍然不知道您坚持使用“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. 我们可以定义ListList a = null | snoc (List a, a)。但是,使用它更自然cons。现在,根据我们是使用cons还是snoc定义List数据类型,将新元素放在列表前面或将新元素放在列表后面会变得很昂贵:

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

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

差异列表用于分摊昂贵操作的成本,方法是延迟列表的连接直到需要,然后以最有效的顺序连接它们。例如,假设我们有as ++ bs ++ cs ++ ds连接四个列表的表达式。如果我们使用 的cons实现,List那么最有效的连接顺序是as ++ (bs ++ (cs ++ ds)),这就是为什么(++)Haskell 中的运算符是右结合的。另一方面,如果我们使用 的snoc实现,List那么最有效的连接顺序是((as ++ bs) ++ cs) ++ ds.

当使用 的cons实现时List,差异列表的形式为(xs ++)where xsis a regular list。我们可以使用常规函数组合(即(as ++) . (bs ++) . (cs ++) . (ds ++))将它们组合起来。当使用 的snoc实现时List,差异列表的形式为(++ xs)where xsis a regular list。我们可以使用常规函数组合(即(++ ds) . (++ cs) . (++ bs) . (++ as))将它们反向组合。cons这是使用 的实现List更可取的另一个原因。

现在,让我们换个角度谈谈非空列表的一部分。当涉及到列表时(无论我们使用的是cons实现List还是snoc实现) ,List术语head、和具有非常具体的含义:tailinitlast

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

              init         last
     ┌──────────┴─────────┐  │
snoc(snoc(snoc(null, 1), 2), 3);
                     │   └─┬─┘
                   head  tail
  1. head非空列表的 是列表的第一个元素。
  2. tail非空列表的 是除了列表的第一个元素之外的所有内容。
  3. init非空列表的 是除了列表的最后一个元素之外的所有内容。
  4. last非空列表的 是列表的最后一个元素。

因此,根据我们是使用cons还是snoc定义List数据类型,headandtailinitandlast变得昂贵:

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

无论如何,这就是为什么“Nil 不应该在列表的尾部,而应该在头部”这句话毫无意义的原因。列表的头部是列表的第一个元素。Nil 不是列表的第一个元素。因此,说 nil 应该始终是列表的头部是不合逻辑的。


现在,让我们进入折叠。根据我们是使用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的理由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实际上是在替换 everycons并且func我们正在替换nullinit。这允许您通过折叠第一个列表、用第二个列表cons替换cons和替换来执行诸如附加两个列表之类的操作:nullys = 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.

选择更简单的替代方案。显式提供初始值。正如Python 之禅所说:

美丽总比丑陋好。
显式优于隐式。
简单胜于复杂。
...
特殊情况不足以打破规则。

无论如何,进入最后一部分。

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

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

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

    1. 标识元素是标识函数,不需要提供外部初始值。

    实际上,如果你折叠一个差异列表,那么你仍然需要提供一个初始值。有关参考,请参阅Hackage 包中的foldr功能。Data.DList

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

    1. 不知道为什么一定不是左折而是右折?

    由于您的L(1)(2)(3)语法不可靠,您只能向后构建列表(即L(1)(2)(3) = cons(3, cons(2, cons(1, null))))。因此,如果你想“向前”折叠列表,那么你必须使用foldr而不是foldl. 请注意,如果我们使用snoc而不是consthen 它实际上是转发(即L(1)(2)(3) = snoc(snoc(snoc(null, 1), 2), 3))。这源于一个事实,snoc即只是cons参数被翻转了。因此,foldrforcons等价于foldlfor snoc,反之亦然,这是 user633183 注意到的。

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

  3. 关于您关于 Church 编码列表的第二点:

    1. 不清楚与 Monoids 的关系

    所有列表都是幺半群,其中单位元为null,二元运算为append = (xs, ys) => foldr(cons, ys, xs)。注意foldr(cons, null, xs) = xs(左身份)和foldr(cons, ys, null) = ys(右身份)。此外,foldr(cons, zs, foldr(cons, ys, xs))等价于foldr(cons, foldr(cons, zs, ys), xs)(关联性)。

  4. 关于您关于 Church 编码列表的第三点:

    1. 特别是,Nil 不是标识元素(= 标识函数),示例代码需要提供外部初始值。

    是的,nil 实际上是列表的标识元素。如果List数据类型实现为差异列表,则 nil 是标识函数。否则,就另当别论了。尽管如此,nil 始终是列表的标识元素。

    我们已经讨论过为什么需要外部初始值。如果您不提供它们,那么您将无法执行某些操作,例如append. 您必须提供初始值才能附加两个列表。要么显式提供初始值,要么通过将列表的第一个元素(使用 时foldl)或最后一个元素(使用 时foldr)作为特例处理(从而破坏函数式编程的原则)来人为地提供初始值。

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

    所以,我很好奇的是有没有像 Church-list 这样的 Diffrence 列表的形式化。

    你为什么想这么做?你希望达到什么目标?Church 编码仅在理论上很有趣。在实践中效率不是很高。此外,差异列表仅在您随意连接列表时才有用(从而利用差异列表的单面结构来展平它们)。将两者结合起来是一个非常糟糕的主意。

无论如何,我希望你不要再问这样的问题,花点时间阅读SICP

于 2018-07-24T18:26:54.670 回答
2

不知道为什么一定不是左折而是右折?

没有“它不能是左折叠”或“它必须是右折叠”这样的事情。我的实现是一种选择,我为您提供了一个非常小的程序,让您有信心自己做出选择

不清楚与 Monoids 的关系

我给出的实现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 不是标识元素(= 标识函数),示例代码需要提供外部初始值。

不,nil是如上所示的标识元素。


一般来说,您的一连串问题似乎是关于在不使用 JavaScript 复合数据[]{}.

实际上,有无数种方法可以实现列表。当然有很多传统的设计,但如果你的目标是自己创造一个,那么没有“最好”甚至“更好”的类型。每个实现都是围绕一组标准设计的。

差异列表和 Church 的右折叠列表只是两种可能的编码。我们可以对简化列表使用完全不同的编码——</p>

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)

请注意,尽管这些实现与之前的实现基本相同,但它们nil构建cons了一个完全不同的数据结构。就是数据抽象的力量本质。

length并且toArray不需要改变。我们可以实现 Monoid 接口——</p>

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)

单子?当然——</p>

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形成可靠的合同。与差异列表相同——它只是另一个具有良好定义和可靠行为的实现。每个实现都有自己的性能配置文件,并且在不同的情况下会有不同的表现。

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


lisp/Scheme 教科书中解释的传统构造列表的方法是非常错误的。Nil 不应该在列表的尾部,而应该在头部。Lisp/Scheme 扭曲的列表结构(尾部为 0 = nil)给编程世界带来了如此多的混乱。

你不知道你在说什么

于 2018-07-24T04:36:46.027 回答
-1

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

回复@Aadit M Shah

现在,让我们进入折叠。根据我们是使用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的理由snoc

作为结构递归的 snoc 的折叠是自然的,我想在那里分享答案。 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实际上是在替换 everycons并且func我们正在替换nullinit。这允许您通过折叠第一个列表、用第二个列表cons替换cons和替换来执行诸如附加两个列表之类的操作:nullys = 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.

好吧,我真的不明白你为什么要这样做,而我写了一个没有初始值的代码,并且反驳了这一系列“应该提供初始值”的观点。

我已经展示了我的代码的一部分,但同样,这里是一个方法:

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

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

log(
    list3(sum)
);

当你硬编码“初始值”时,你到底在做什么?

例如,对于“加”操作,你应该如何选择初始值0

它是从哪里来的吗?从来没有,事实上0,初始值是由二元运算符本身定义的。

在你的脑海里,你想,“好吧 0+a = a = a + 0 ,所以这一定是初始值!”,

或者你想,“好吧 1 * a = a = a * 1,所以这一定是!”,

或者你想,“好吧,[].concat(a) = [a],所以 [] 必须是初始值!”

正确的?你在做什么?您只是在脑海中拿起身份元素,这绝对是胡说八道,因为我们使用计算机并编写代码!

如果你真正需要的是标识元素,那么编码。至少,我做到了。

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

如果它I 反映了底部对的正确值,因为 I 是一个 identity I = a=>a,实际上我可以将代码重写为:

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

请注意,由于它击中底部对,循环操作:

plus(left(sum))(right)变成(left)(right)

这样,我们就不必浪费我们的大脑操作来识别明显的初始值,例如01[],它们基本上是同一值。

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

可以定义二元运算符来识别独立于左/右折叠实现的第一个/最后一个。

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??
于 2018-07-25T03:54:40.550 回答
-1

我的实现:

Identity/Nil 在头部而不是尾部

不需要硬编码初始值。

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 ]

基于这个实现,我想回应

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

问题的根源

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

正如我们已经确认的那样,仅通过函数构造列表并没有错。Church 编码是一种用柯里化函数构造一切的方式。所以这个说法是无效的。

这种语法没有任何意义,人们一次又一次地告诉你不要使用这种语法:

如果你坚持某件事没有任何意义的原因是因为“人们一次又一次地告诉你放弃”,我不敢说你错了,让我们看看人们是怎么说的。

  1. user633183对您的第一个问题的回答:

函数柯里化和可变参数并不能真正一起工作。一旦您意识到以下两个表达式不兼容,这是一个明显的限制

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

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

类型不匹配问题已经解决,因此,对于L.

  1. Bergi对您的第二个问题的评论:

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

同样,类型不匹配问题已经解决,因此,对于L.

  1. user633183对您的第三个问题的回答:

所以说到类型,让我们来看看autoCons——</p>

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,并且最后,请注意不是我实现了 makeL返回一个裸值,没有包裹 by L

当您可以简单地编写时,我看不出任何使用该L(1)(2)(3)语法的充分理由toList([1,2,3])

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

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

我必须稍后评论效率,但到目前为止,当现有方法已经自然而简单地实现它时,为什么实际上有人必须向后实现翻转列表?你怎么能证明打破简单性只是为了支持发烧使用“正常列表”?“正常”是什么意思?

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

列表的代数结构

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

  1. 首先,您认为列表的头部应该始终为 nil:

lisp/Scheme 教科书中解释的传统构造列表的方法是非常错误的。Nil 不应该在列表的尾部,而应该在头部。Lisp/Scheme 扭曲的列表结构(尾部为 0 = nil)给编程世界带来了如此多的混乱。

正确的。事实上,我还有一些我还没有定义的原因。我稍后会定义。

  1. 其次,您认为不必为列表折叠提供初始值:

我仍然不知道您坚持使用“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);

我们应该引入“Nil on the each tails”,因为它更容易读写?认真的吗?我不会同意,我想知道其他人的感受。

好吧,表达以下结构

一个 List ofa是 null 或一个a和另一个 List of的缺点a

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

const list2 = pair(list1)(2);

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

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

在此处输入图像描述

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

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

注意:使用snoc. 我们可以定义ListList a = null | snoc (List a, a)。但是,使用它更自然cons。现在,根据我们是使用cons还是snoc定义List数据类型,将新元素放在列表前面或将新元素放在列表后面会变得很昂贵:

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

“落后”的成本相当明显,或者追加更有优势。我们很少需要将新数据添加到现有列表中。

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

cons差异列表...这是使用 的实现List更可取的另一个原因。

诸如运营成本差异之类的 hack 要求是“snoc”不需要的 hack。所以我真的不明白你认为存在变通方法是优势的观点。

现在,让我们换个角度谈谈非空列表的一部分。当涉及到列表时(无论我们使用的是cons实现List还是snoc实现) ,List术语head、和具有非常具体的含义:tailinitlast

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

              init         last
     ┌──────────┴─────────┐  │
snoc(snoc(snoc(null, 1), 2), 3);
                     │   └─┬─┘
                   head  tail
  1. head非空列表的 是列表的第一个元素。
  2. tail非空列表的 是除了列表的第一个元素之外的所有内容。
  3. init非空列表的 是除了列表的最后一个元素之外的所有内容。
  4. last非空列表的 是列表的最后一个元素。

因此,根据我们是使用cons还是snoc定义List数据类型,headandtailinitandlast变得昂贵:

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

没错,这是一个常见的场景,代码需要一个新的数据 =“last”和累积的数据 =“init”,并且在我自己的代码中实现它非常容易,因为“snoc”/pair 提供了left(“init”)和right(“最后一个”)成本低廉。

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

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

log(
    list3(sum)
);

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

当然,简单性来自于Plus二元运算符的顺序运算和pair(“snoc”)之间的相同结构。

//`1 + 2 + 3` 

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

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

无论如何,这就是为什么“Nil 不应该在列表的尾部,而应该在头部”这句话毫无意义的原因。列表的头部是列表的第一个元素。Nil 不是列表的第一个元素。因此,说 nil 应该始终是列表的头部是不合逻辑的。

我觉得没有任何理由选择更复杂的结构,尤其是对于初学者。

事实上,这个词cons无处不在,另一方面,snoc却很难找到。

https://en.wikipedia.org/wiki/Cons 一个字都没有描述snoc,当然也没有解释。我认为这是非常不健康的情况。这里发生了什么??

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

我真的很抱歉,但我可能应该指出一部分责任实际上是你的,非常有经验的程序员和像你们这样热情的伟大导师出于某种原因高估cons和低估了snoc

如果我是老师给孩子们教列表,首先介绍哪种结构?“斯诺克”。它直截了当,更易于理解和使用。

类似于顺序二元运算。

//`1 + 2 + 3` 

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

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

简单的。

缺点?尼尔斯很难。


我会将其余的回复分开到另一个帖子,因为这太长了。=>

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

于 2018-07-24T14:09:15.003 回答