flink fold
最近我们在代码审查期间进行了长时间的讨论,不管scala.Option.fold()
是惯用的还是聪明的,或者是scala.Option.fold()
和棘手的? 让我们首先描述问题所在。 Option.fold
有两件事:将函数f
映射到Option
的值(如果有)上,或者在不存在的情况下返回替代alt
。 使用简单的模式匹配,我们可以如下实现它:
val option: Option[T] = //...
def alt: R = //...
def f(in: T): R = //...
val x: R = option match {
case Some(v) => f(v)
case None => alt
}
如果您喜欢getOrElse
, fold
实际上是map
和getOrElse
的组合:
val x: R = option map f getOrElse alt
或者,如果您仍然是C程序员,但仍想使用Cala编写Scala编译器,请执行以下操作:
val x: R = if (option.isDefined)
f(option.get)
else
alt
有趣的是,这类似于fold()
的实际实现方式,但这是一个实现细节。 好的,以上所有内容都可以用单个Option.fold()
代替:
val x: R = option.fold(alt)(f)
从技术上讲,您甚至可以使用/:
和\:
运算符( alt /: option
)–但这只是受虐狂。 我在使用option.fold()
惯用语时遇到三个问题。 首先–除了可读性之外,什么都没有。 我们正在折叠(减少) Option
–的确没有多大意义。 其次,它通过以失败(不存在, alt
)条件开始,然后跟随状态块( f
函数;另请参见:将map-getOrElse重构为fold ) 来逆转普通的正则负情况 。 有趣的是,如果将其命名为mapOrElse
则该方法对我来说非常mapOrElse
:
/**
* Hypothetical in Option
*/
def mapOrElse[B](f: A => B, alt: => B): B =
this map f getOrElse alt
实际上,Scalaz中已经有这样的方法,称为OptionW.cata
。 卡塔 这是马丁·奥德斯基(Martin Odersky)所说的 :
“我个人发现像
cata
这样的方法需要两个闭包,因为参数通常会夸大它。 您真的通过map
+getOrElse
获得了可读性吗? 想一想您的代码的新手[…]”
尽管cata
具有一定的理论背景 ,但Option.fold
听起来像是随机的名称冲突,除了混乱之外,什么都没有带来。 我知道您会说什么, TraversableOnce
已经fold
,我们正在做同样的事情。 为什么是随机冲突而不是扩展TraversableOnce
描述的协定? Scala集合中的fold()
方法通常仅委托foldLeft()
/ foldRight()
(对于给定的数据结构更好地工作)之一,因此它不能保证顺序,并且折叠功能必须是关联的。 但是在Option.fold()
,契约是不同的:折叠函数只接受一个参数,而不是两个。 如果您阅读我以前的关于折痕的文章,则知道折减函数始终采用两个参数:当前元素和累加值(第一次迭代期间的初始值)。 但是Option.fold()
仅接受一个参数:当前Option
值! 这破坏了一致性,尤其是当实现Option.foldLeft()
和Option.foldRight()
具有正确的协定时(但这并不意味着它们更具可读性)。
理解option折叠的唯一方法是将Option
想象为具有0
或1
元素的序列。 那有点道理,对吗? 没有。
def double(x: Int) = x * 2
Some(21).fold(-1)(double) //OK: 42
None.fold(-1)(double) //OK: -1
但:
Some(21).toList.fold(-1)(double)
<console>: error: type mismatch;
found : Int => Int
required: (Int, Int) => Int
Some(21).toList.fold(-1)(double)
^
如果我们将Option[T]
视为List[T]
,则尴尬的Option.fold()
会中断,因为它的类型与TraversableOnce.fold()
。 这是我最大的担心。 我不明白为什么不按类型系统(特征?)定义折叠并严格执行。 作为一个例子看看:
在Haskell中
Data.Foldable
类型类描述Haskell中的各种折叠Data.Foldable
。 在Scala中有熟悉的foldl
/ foldr
/ foldl1
/ foldr1
,因此命名为foldLeft
/ foldRight
/ reduceLeft
/ reduceRight
。 它们具有与Scala相同的类型,并且对所有可以折叠的类型(包括Maybe
,列表,数组等)的行为都不足为奇。还有一个名为fold
的函数,但它的含义完全不同:
class Foldable t where
fold :: Monoid m => t m -> m
尽管其他折叠非常复杂,但此折叠几乎不带一个m
s的可折叠容器(必须为Monoid
s)并返回相同的Monoid
类型。 快速回顾:如果类型存在一个中性值,并且一个操作使用两个值而只产生一个,则该类型可以是Monoid
。 在其中一个参数为中性值的情况下应用该函数会产生另一个参数。 String
( [Char]
)是一个很好的示例,其中空字符串为中性值( mempty
),字符串连接为此类操作( mappend
)。 请注意,可以使用两种不同的方法来构造数字的单素体:在中性值为0
( x + 0 == 0 + x == x
对于任何x
)的情况下以及与中性1
乘法( x * 1 == 1 * x == x
对于任何x
)。 让我们坚持下去。 如果我折叠空字符串列表,我将得到一个空字符串。 但是,当列表包含许多元素时,它们将被串联起来:
> fold ([] :: [String])
""
> fold [] :: String
""
> fold ["foo", "bar"]
"foobar"
在第一个示例中,我们必须显式说明空列表[]
的类型。 否则,Haskell编译器将无法弄清列表中元素的类型,因此无法选择哪个Monoid实例。 在第二个示例中,我们声明从fold []
返回的任何内容都应该是String
。 据此,编译器推断[]
实际上必须具有[String]
类型。 最后fold
是最简单的:程序会折叠列表中的元素并将其连接起来,因为连接是在Monoid String
类型类实例中定义的操作。
返回选项(或更确切地说, Maybe
)。 折叠具有类型参数为Monoid
Maybe
monad(我不能相信我刚刚说过)有一个有趣的解释:它要么返回Maybe
内部的值,要么返回默认的Monoid
值:
> fold (Just "abc")
"abc"
> fold Nothing :: String
""
Just "abc"
与Scala中的Some("abc")
相同。 您可以在此处看到,如果Maybe String
为Nothing
,则返回中性的String
monoid值,即为空字符串。
摘要
Haskell表明折叠(也Maybe
超过Maybe
)至少可以保持一致。 在Scala中, Option.fold
与Option.fold
无关,令人困惑List.fold
可读。 我建议您避免使用它,而应该使用更详细的map
/ getOrElse
转换或模式匹配。
PS:我是否提到过还有Either.fold()
(甚至有不同的合同),但是没有Try.fold()
吗?
翻译自: https://www.javacodegeeks.com/2014/06/option-fold-considered-unreadable.html
flink fold