【问题标题】:OCaml - Accumulator Using Fold LeftOCaml - 使用向左折叠的累加器
【发布时间】:2016-03-31 18:57:31
【问题描述】:

here学习OCaml。

我想验证我是否理解这个 OCaml 代码的 sn-p 是如何工作的

List.fold_left (fun acc x -> acc + x) 0 [ 1; 2; 3; 4 ]

我有一种直觉,这相当于 Python 中的 reduce 函数。具体来说,我认为它相当于

reduce(lambda x, y: x + y, [1, 2, 3])

匿名函数采用两个参数 - accx,并返回单个值 acc + x。我知道最初,第一个参数 acc 将为 0 但它怎么知道第二个参数必须是列表的第一个元素?

我认为正在发生的事情是fold_left 向匿名函数提供了两个参数,然后使用新参数递归调用自身,直到列表变为空。

为了确认这一点,我看到了this

当我定义像 let inc x = x + 1 这样的函数时,我会得到像 val inc : int -> int = <fun> 这样的东西,但在这种情况下,签名是 : ('a -> 'b -> 'a) -> 'a -> 'b list -> 'a = <fun>

什么是'a,我应该如何解释这个函数签名,使List.fold_right f [a1; ...; an] b变成f a1 (f a2 (... (f an b) ...))

【问题讨论】:

    标签: functional-programming ocaml


    【解决方案1】:

    你问了很多问题。

    我很确定 Python reduce 是一个折叠,所以你的直觉可能是正确的。

    你问“它怎么知道第二个参数必须是列表的第一个元素?”不幸的是,我认为这不是一个格式良好的问题。没有“它”知道任何事情。 fold_left 的定义很可能给出了答案。它知道该怎么做,因为有人这样写代码:-)

    这是标准库中fold_left的定义:

    let rec fold_left f accu l =
      match l with
        [] -> accu
      | a::l -> fold_left f (f accu a) l
    

    从某种意义上说,这应该回答了你所有的问题。

    fold_left 类型中的'a 类型是累加器的类型。关键是您可以使用任何类型的累加器。这就是折叠如此强大的原因。只要它与折叠函数接受和返回的值匹配,它就可以是任何你想要的。

    【讨论】:

    • 那么OCaml什么时候知道函数的参数是'a还是int
    • 简短回答:Hindley-Milner 打字的关键定理说,任何你无法确定类型的函数参数或类型参数(有重要限制)都是多态参数。这里有一些解释:OCaml Type Inference 和这里Hindley-Milner Type System
    【解决方案2】:

    如果我没记错的话,reduce 是 fold 的一个更简单的版本,它将列表的第一个元素作为起始元素。我会这样定义它:

    let reduce f = function 
       | x::xs -> fold_left f x xs
       | [] -> failwith "can't call reduce on empty lists!"
    

    如果在OCaml中输入,会显示其类型:

    val reduce : ('a -> 'a -> 'a) -> 'a list -> 'a 
    

    你可以和 fold_left 的类型对比一下:

    ('b -> 'a -> 'b) -> 'b -> 'a list -> 'b
    

    这里的类型变量'a'b表示它们可以代表任何类型。在您的示例中,'a'b 都变为 int。如果我们插入类型,fold_left 有签名:

    (int -> int -> int) -> int -> int list -> int
    

    这就是我们所期望的:+ 是一个接受两个整数并返回一个新整数的函数,0 是一个整数,[1;2;3;4;] 是一个整数列表。 fold_left 有两个类型变量而reduce 只有一个的情况已经暗示它更通用。要了解为什么我们可以查看reduce 的定义。由于折叠的起始元素是列表的元素,'a''b 的类型必须相同。这对于总结元素很好,但是说,我们想为我们的总结构建一个抽象语法树。我们为此定义了一个类型:

    type exp = Plus of exp * exp | Number of int 
    

    那么我们可以调用:

    fold_left (fun x y -> Plus (x, (Number y))) (Number 0) [1; 2; 3; 4]
    

    导致表达式:

    Plus (Plus (Plus (Plus (Number 0, Number 1), Number 2), Number 3), Number 4)
    

    这棵树的一个好处是您可以很好地看到首先应用的内容(0 和 1) - 在相加的情况下这不是问题,因为它是关联的(这意味着 a+(b+c) = (a +b)+c) 这不是减法的情况(比较例如 5-(3-2) 和 (5-3)-2)。

    如果你想用 reduce 做类似的事情,你会注意到 OCaml 会抱怨类型错误:

    reduce (fun x y -> Plus (x, (Number y))) [1; 2; 3; 4] ;; Error: This expression has type exp but an expression was expected of type
    int

    在这种情况下,我们可以将每个整数包装为输入列表中的表达式,然后类型一致。由于我们已经有了 Numbers,我们不需要在 y 中添加 Number 构造函数:

    let wrapped = map (fun x -> Number x) [1; 2; 3; 4] in
    reduce (fun x y -> Plus (x, y)) wrapped
    

    同样,我们得到了相同的结果,但我们需要对map 进行额外的函数调用。在fold_left 的情况下,这不是必需的。

    P.S.:您可能已经注意到 OCaml 将 fold_left 的类型指定为 ('a -> 'b -> 'a) -> 'a -> 'b list -> 'a。我想你很快就会意识到类型变量的名称不起作用。为了便于比较,我切换了名称,使函数始终应用于'a 列表。

    【讨论】:

      【解决方案3】:

      有点晚了,但是如果您合并 reduceinitializer 参数,OCaml 的折叠和 Python 的 reduce 之间的比较可能会更容易。

      使用折叠对 OCaml 中的整数列表求和:

      let sum = List.fold_left (+) 0 [1; 2; 3]
      

      并在 Python 中使用 reduce

      from functools import reduce
      
      sum = reduce(int.__add__, [1, 2, 3], 0)
      

      在这里你可以看到参数的顺序有点不同,但它们都在那里。

      Python 觉得你不太可能需要初始化器,所以为了方便起见,把它作为一个可选参数放在最后。 OCaml 将列表作为最后一个参数也是为了方便,因为部分应用程序可以轻松编写类似sum 函数的东西。

      let sum = List.fold_left (+) 0
      

      而不是:

      let sum lst = List.fold_left (+) 0 lst
      

      【讨论】:

        猜你喜欢
        • 2014-09-03
        • 2017-04-11
        • 1970-01-01
        • 2022-01-08
        • 1970-01-01
        • 1970-01-01
        • 2021-04-19
        • 2023-02-15
        • 2012-01-19
        相关资源
        最近更新 更多