为啥在 Racket 中以一种奇怪的方式定义 foldl?
Posted
技术标签:
【中文标题】为啥在 Racket 中以一种奇怪的方式定义 foldl?【英文标题】:Why is foldl defined in a strange way in Racket?为什么在 Racket 中以一种奇怪的方式定义 foldl? 【发布时间】:2012-02-05 09:38:37 【问题描述】:在 Haskell 中,与许多其他函数式语言一样,函数 foldl
被定义为例如 foldl (-) 0 [1,2,3,4] = -10
。
这没关系,因为根据定义,foldl (-) 0 [1, 2,3,4]
是 ((((0 - 1) - 2) - 3) - 4)
。
但是,在 Racket 中,(foldl - 0 '(1 2 3 4))
是 2,因为 Racket “智能”计算如下:(4 - (3 - (2 - (1 - 0))))
,确实是 2。
当然,如果我们定义辅助函数flip,像这样:
(define (flip bin-fn)
(lambda (x y)
(bin-fn y x)))
然后我们可以在 Racket 中实现与在 Haskell 中相同的行为:而不是 (foldl - 0 '(1 2 3 4))
,我们可以写成:(foldl (flip -) 0 '(1 2 3 4))
问题是:为什么球拍中的foldl
以如此奇怪(非标准和不直观)的方式定义,与任何其他语言不同?
【问题讨论】:
FWIW,Chez Scheme 的fold-left
与您的预期一致:(fold-left - 0 '(1 2 3 4))
是 -10
和 (fold-left cons '() '(1 2 3 4))
是 ((((() . 1) . 2) . 3) . 4)
。
【参考方案1】:
来自球拍documentation,foldl
的描述:
(foldl proc init lst ...+) → any/c
提到了您的问题的两个兴趣点:
输入的lst从左到右遍历
和
foldl 处理常量空间中的 lsts
为了简单起见,我将推测它的实现可能是什么样子,用一个列表:
(define (my-foldl proc init lst)
(define (iter lst acc)
(if (null? lst)
acc
(iter (cdr lst) (proc (car lst) acc))))
(iter lst init))
如您所见,满足了从左到右遍历和常量空间的要求(注意iter
中的尾递归),但是proc
的参数的顺序从未在描述中指定。因此,调用上述代码的结果是:
(my-foldl - 0 '(1 2 3 4))
> 2
如果我们以这种方式指定proc
的参数顺序:
(proc acc (car lst))
那么结果就是:
(my-foldl - 0 '(1 2 3 4))
> -10
我的意思是,foldl
的文档没有对proc
的参数的评估顺序做出任何假设,它只需要保证使用常量空间并且评估列表中的元素从左到右。
作为旁注,您可以通过简单地编写以下内容来为您的表达式获得所需的评估顺序:
(- 0 1 2 3 4)
> -10
【讨论】:
就我个人而言,我更希望函数的描述具体说明它返回的结果,而不是它使用了多少堆栈空间! @Ben the doc does show an example application of> (foldl cons '() '(1 2 3 4))
==> '(4 3 2 1)
只能使用一个特定的 args 顺序。我同意,他们可以再强调一点。【参考方案2】:
Racket 的 foldl
和 foldr
(以及 SRFI-1's fold
和 fold-right
)具有以下属性
(foldr cons null lst) = lst
(foldl cons null lst) = (reverse lst)
我推测是因为这个原因选择了参数顺序。
【讨论】:
【参考方案3】:Haskell 定义不统一。在 Racket 中,两个折叠的函数具有相同的输入顺序,因此您可以将 foldl
替换为 foldr
并获得相同的结果。如果您使用 Haskell 版本执行此操作(通常),您会得到不同的结果——您可以在两者的不同类型中看到这一点。
(事实上,我认为为了进行适当的比较,您应该避免这些类型变量都是整数的玩具数字示例。)
这有一个很好的副产品,鼓励您根据语义差异选择foldl
或foldr
。我的猜测是,使用 Haskell 的命令,您可能会根据操作进行选择。你有一个很好的例子:你使用了foldl
,因为你想减去每个数字——这是一个“显而易见”的选择,很容易忽略foldl
在懒惰的人中通常是一个糟糕的选择这一事实语言。
另一个区别是 Haskell 版本比 Racket 版本更受限制:它只对一个输入列表进行操作,而 Racket 可以接受任意数量的列表。这使得输入函数具有统一的参数顺序变得更加重要)。
最后,假设 Racket 与“许多其他函数式语言”不同是错误的,因为折叠远非新技巧,而且 Racket 的根源远比 Haskell(或这些其他语言)古老。问题因此可以换个方式:为什么 Haskell 的 foldl
会以一种奇怪的方式定义?(不,(-)
不是一个好的借口。)
历史更新:
由于这似乎一次又一次地打扰人们,所以我做了一些跑腿工作。这不是绝对的,只是我的二手猜测。如果您了解更多,甚至更好,请随时编辑此内容,向相关人员发送电子邮件并询问。具体来说,我不知道做出这些决定的日期,所以下面的列表是粗略的。
首先是 Lisp,没有提到任何形式的“折叠”。相反,Lisp 有reduce
,这是非常不统一的,特别是如果您考虑它的类型。例如,:from-end
是一个关键字参数,用于确定是左扫描还是右扫描并且它使用不同的累加器函数,这意味着累加器类型取决于该关键字。这是对其他技巧的补充:通常第一个值取自列表(除非您指定 :initial-value
)。最后,如果您没有指定:initial-value
,并且列表为空,它实际上会将函数应用于零参数以获得结果。
所有这些都意味着reduce
通常用于顾名思义:将值列表减少为单个值,其中两种类型通常相同。这里的结论是,它的用途与折叠类似,但它不如通过折叠获得的通用列表迭代构造有用。我猜这意味着reduce
和后来的折叠操作之间没有很强的关系。
遵循 Lisp 并具有适当折叠的第一个相关语言是 ML。正如下面 newacct 的回答所指出的那样,在那里做出的选择是使用 uniform types version(即 Racket 使用的)。
下一个参考文献是 Bird & Wadler 的 ItFP (1988),它使用 different types(如在 Haskell 中)。但是,他们note in the appendix 认为 Miranda 具有相同的类型(如在 Racket 中)。
Miranda 后来在switched the argument order(即,从 Racket 订单转移到 Haskell 订单)。具体来说,该文本说:
警告 - foldl 的定义与旧版本的 Miranda 不同。这里的一个与 Bird and Wadler (1988) 中的相同。旧定义将 'op' 的两个参数颠倒了。
Haskell 从 Miranda 那里得到了很多东西,包括不同的类型。 (但当然我不知道日期,所以米兰达的变化可能是由于 Haskell 造成的。)无论如何,目前显然没有达成共识,因此上述相反的问题成立。
李>OCaml 遵循 Haskell 方向,使用 different types
我猜“如何设计程序”(又名 HtDP)大约是在同一时期编写的,他们选择了same type。然而,没有任何动机或解释——事实上,在那次练习之后,它被简单地称为one of the built-in functions。
Racket 对fold operations 的实现当然是这里提到的“内置”。
然后是SRFI-1,选择使用相同类型的版本(如球拍)。这个决定was question 由 John David Stone 做出,他指出 SRFI 中的一条评论说
注意:MIT Scheme 和 Haskell 翻转 F 的
reduce
和fold
函数的 arg 顺序。
奥林后来addressed this:他只是说:
好点,但我想要两个函数之间的一致性。 状态值优先:srfi-1, SML 状态值最后:Haskell
特别注意他对 state-value 的使用,这表明一致类型可能比运算符顺序更重要。
【讨论】:
对于它的价值,我认为推理是这样的:对于 cons 列表,foldr
恰恰是自然的变态,因此它的第一个参数的类型为 a -> b -> b
,而不是带有构造函数 @ 的列表987654350@。另一方面,foldl
正是 snoc 列表的自然变态,反向构建,因此第一个参数具有 b -> a -> b
用于虚构构造函数 Snoc :: [a] -> a -> [a]
。
“Haskell 的定义并不统一”——但确实如此!存在x
、y
,这样foldl (foldl f) x y
对Haskell 中的所有f
都是正确类型的,foldr (foldr f) x y
也是如此。 Racket 的 foldl 不是这样! 当然这在 Racket 中并不重要(没有柯里化),但柯里化在 ML 派生的语言中很重要,因此 Haskell 中使用的顺序很重要。当然可以说 foldl 和 foldr 都应该只使用 Haskell 的 foldr arg 顺序。 (顺便说一句,Eli——我曾就此与 SK 进行过长时间的讨论,这是我能够改变他想法的少数几次之一!)
foldl 在惰性语言中“通常”不是一个糟糕的选择——尤其是当您使用它来计算单个数字、字符串等时。如果你需要那个数字,你就需要它,没有办法评估整个列表,无论是从右边还是从左边。
@Eli - foldl 的定义在 Haskell 中是尾递归的,因此它不使用任何堆栈空间。问题有时是当 foldl 构建一个代表结果的大 thunk 时。在 foldl 完成后评估该 thunk 时,可能会发生堆栈溢出。在需要恒定堆栈空间的情况下使用 foldl'。顺便说一句,未评估的 thuk 可能会发生同样的问题 foldr f 其中 f 在其第二个参数中是严格的。
@Ingo 如果foldr
中的f
在第二个arg中是严格的,则arg的eval只会在进入f
之后被触发,所以不会有任何涉及的 thunk,只有堆栈(当然可以溢出(......只是挑剔))。【参考方案4】:
“不同于任何其他语言”
作为反例,Standard ML(ML 是一种非常古老且有影响力的函数式语言)的 foldl
也以这种方式工作:http://www.standardml.org/Basis/list.html#SIG:LIST.foldl:VAL
【讨论】:
以上是关于为啥在 Racket 中以一种奇怪的方式定义 foldl?的主要内容,如果未能解决你的问题,请参考以下文章