问题 有没有简洁的方法来“反转”一个选项?


假设我有一个可以接受可选参数的函数,我想返回一个 Some 如果论证是 None 和a None 如果论证是 Some

def foo(a: Option[A]): Option[B] = a match {
  case Some(_) => None
  case None    => Some(makeB())
}

所以我想要做的就是反过来 map。的变种 orElse 不适用,因为它们保留了价值 a 如果它存在。

有没有更简洁的方法来做到这一点 if (a.isDefined) None else Some(makeB())


6706
2018-03-23 16:40


起源

出于好奇,接受选项作为参数的用例是什么?通常情况下,我总是将选项作为参数删除,并在选项上调用map,然后将其传递给将采用未装箱选项的函数 - Jordan Cutler
我其实很喜欢 if(a.isDefined) ...:清晰,尽可能简洁 - Juh_
在我当前的特定用例中,我正在生成X.509证书作为测试数据,并希望生成并输出CA证书以及主题证书 除非 主题证书作为参数。 - Emil Lundberg
这似乎是一个奇怪的API接受一个 Some 但忽略它包含的内容。这感觉更像是布尔。你能解释用例(特别是你对结果做了什么)?也是 makeB() 纯?在那种情况下,甚至更奇怪。 - The Archetypal Paul
这个 是用例。它是用于生成测试数据的内部API。注意该方法如何接受 attestationCertAndKey: Option[(X509Certificate, PrivateKey)]并返回一个 Option[X509Certificate],后者(CA证书)是 Some 当且仅当前者(主题证书)是 None。换句话说,如果给出主题证书,则不会签名,但如果没有签名,则会生成并签名。 - Emil Lundberg


答案:


这个答案概述:

  1. 单线解决方案使用 fold
  2. 与小的演示 fold
  3. 讨论为什么 fold-解 可以 和...一样“明显” if-else-解。

你可以随时使用 fold 改造 Option[A] 你想要的任何东西:

a.fold(Option(makeB())){_ => Option.empty[B]}

演示

这是一个完整的可运行示例,包含所有必需的类型定义:

class A
class B
def makeB(): B = new B

def foo(a: Option[A]): Option[B] = a match {
  case Some(_) => None
  case None    => Some(makeB())
}

def foo2(a: Option[A]): Option[B] = 
  a.fold(Option(makeB())){_ => Option.empty[B]}

println(foo(Some(new A)))
println(foo(None))
println(foo2(Some(new A)))
println(foo2(None))

这输出:

None
Some(Main$$anon$1$B@5fdef03a)
None
Some(Main$$anon$1$B@48cf768c)

为什么 fold 只要 似乎 不太直观

在评论中,@ TheArchetypalPaul评论说 fold 似乎“不那么明显”了 if-else 解。

我会声称这主要是因特殊存在而产生的神器 if-else 布尔语的语法。

如果有像标准的东西

def ifNone[A, B](opt: Option[A])(e: => B) = new {
  def otherwise[C >: B](f: A => C): C = opt.fold((e: C))(f)
}

可以像这样使用的语法:

val optStr: Option[String] = Some("hello")

val reversed = ifNone(optStr) { 
  Some("makeB") 
} otherwise {
  str => None
}

更重要的是,如果在过去半个世纪发明的每种编程语言的每一个介绍的第一页上都提到了这种语法,那么 ifNone-otherwise 解决方案(即 fold),对大多数人来说看起来会更自然。

的确如此 Option.fold 方法是 消除 的 Option[T] 类型:每当我们有一个 Option[T] 并希望得到一个 A 出于它,最明显的期望应该是a fold(a)(b) 同 a: A 和 b: T => A。与布尔人的特殊待遇相比 if-else-syntax(这仅仅是一种惯例), fold 方法很基础,事实就是如此 一定是在那里 可以源于第一原则。


6
2018-03-23 16:49



是的,你可以但是 a.fold(Option(makeB())){_ => Option.empty[B]} (对我而言)比不那么明显 if/else OP的。 - The Archetypal Paul
@TheArchetypalPaul我很害怕 if-else 更加“明显”,因为你已经习惯了它。如果你对待 Boolean 和 Option[T] 对称,不给 Boolean 任何偏好,因为它已经50岁了,那么这就是你得到的: if(b) a1 else a2 是规范的消除者 Boolean 哪个,给定 a1: A 和 a2: A 可以改变任何 b: Boolean 成 A。类似地, o.fold(a1)(a2) 同 a1: A, a2: T => A 是 规范的消除器 的 Option[T]。如果你有 Option[T] 并想要一个 A, fold 绝对是最期望的规范。 - Andrey Tyukin
@TheArchetypalPaul ......不对称来自于这样的事实 Boolean 是非常普遍的,它有自己的 if-else 语法定义中的句法糖。如果为每个布尔而不是 if (b) x else y 你必须写 b.fold(x, y)那么 if-isEmpty-elseOP提出的构建将转化为 o.isEmpty.fold(Option.empty[B], Option(makeB())),这并不比直接更清楚 o.fold(Option(makeB())){_ => Option.empty[B]}。这一切都只是因为有 if-else 布尔语的语法糖。 - Andrey Tyukin
是的,“if / then / else”可能是历史性的东西,但它在编程语言中的历史原因恰恰是因为即使在您学习了很多语言之前,其含义也非常明显 - 即使不是程序员,每个人都熟悉这个想法。我害怕我认为“折叠”(以及其他FP概念)的说法更加直观,就像我们过去常常听到的关于面向对象的说法是世界的方式:地图与道路的混淆。 - The Archetypal Paul
换句话说,你的论证似乎是,如果我们删除使if / then / else更明显的合成糖,那么它就不再那么明显了。这是真的,但我突然忽略了这一点。 - The Archetypal Paul


fold比模式匹配更简洁

 val op:Option[B] = ...

 val inv = op.fold(Option(makeB()))(_ => None)

8
2018-03-23 16:46



好主意,但如果我尝试编译它,它给出:错误: object None does not take type parameters。没有写出来,我无法绕过它 Option 明确?.. - Andrey Tyukin
好的捕获,intellij再次失败了,谢谢 - Nazarii Bardiuk
凉!这确实很简洁,但我不确定我是否愿意阅读而不是 if (isDefined) ... 解决方案...... :) - Emil Lundberg


我想出了这个定义 a.map(_ => None).getOrElse(Some(makeB()))

scala> def f[A](a: Option[A]) = a.map(_ => None).getOrElse(Some(makeB()))
f: [A](a: Option[A])Option[makeB]

scala> f(Some(44))
res104: Option[makeB] = None

scala> f(None)
res105: Option[makeB] = Some(makeB())

1
2018-03-23 18:20



请注意,有身份 a.map(f).getOrElse(n) = a.fold(n)(f)。 - Andrey Tyukin
@AndreyTyukin你是说我刚申请了 fold 法律甚至没有意识到它? :P - nicodp
是的。恭喜你,你已经达到了这样一个状态,即你在没有意识到的情况下即时自动发明了catamorphisms。很好! :D当你编程太多哈斯克尔时会发生什么:“哎呀,我是不是偶然重新发明了F-Algebras?” ;) - Andrey Tyukin
感谢:D!希望很快达到疟疾的状态! Haskell FTW - nicodp
这是因为 a.getOrElse(n) = a.fold(n)(identity)。实际上,这是一种“合理设计API”的有趣方法:如果有一些可以通过某些规范函数插入的间隙,则插入折叠,然后插入规范函数,将结果重命名为某些东西。没想过 getOrElse关于这种合理构建的捷径。 - Andrey Tyukin