在功能编程中,有两个重要的方法foldLeft
和foldRight
。以下是foldRight
sealed trait List[+A]
case object Nil extends List[Nothing]
case class Cons[+A](head: A, tails: List[A]) extends List[A]
def foldRight[A, B](ls: List[A], z: B)(f: (A, B) => B): B = ls match {
case Nil => z
case Cons(x, xs) => f(x, foldRight(xs, z)(f))
}
以下是foldLeft
的实施:
@annotation.tailrec
def foldLeft[A, B](ls: List[A], z: B)(f: (B, A) => B): B = ls match {
case Nil => z
case Cons(x, xs) => foldLeft(xs, f(z, x))(f)
}
}
我的问题是:我从很多文档中读到,他们经常把f函数的顺序放在:f: (B, A) => B
而不是f: (A, B) => B
。为什么这个定义更好?因为如果我们使用其他方式,它将与foldLeft
具有相同的签名,并且会更好。
答案 0 :(得分:3)
因为foldLeft
“绕过”遍历中的cons结构:
foldRight(Cons(1, Cons(2, Nil)), z)(f) ~> f(1, f(2, z))
^ ^
A B
foldLeft(Cons(1, Cons(2, Nil)), z)(f) ~> f(f(z, 2), 1)
^ ^
B A
由于它以相反的顺序访问了这些类型,所以这些类型也传统上被翻转。当然可以交换参数,但如果你有一个非交换操作,并期望“传统”的行为,你会感到惊讶。
答案 1 :(得分:1)
...如果我们以其他方式使用,它将与
foldLeft
具有相同的签名,并且会更好。
不,我认为这不会更好。如果它们具有相同的签名,那么如果您打算使用一个签名但不小心输入另一个签名,则编译器将无法捕获它。
A / B顺序也方便地提醒我B
(初始或"零")值与A
元素集合的关系。
foldLeft: B-->>A, A, A, ... // (f: (B, A) => B)
foldRight: ... A, A, A<<--B // (f: (A, B) => B)