一起使用两个 Option 实例的正确方法

Correct way to work with two instances of Option together

当我有一个 Option[T] 实例时,可以很容易地使用 map()flatMap() 等一元操作对 T 执行任何操作。这样我就不必检查它是否已定义或为空,也不必将操作链接在一起最终得到结果 ROption[R]

我的困难是是否有类似优雅的方式在两个 Option[T] 实例上执行功能。

让我们举一个简单的例子,我有两个类型为 Option[Int] 的值,xy。如果它们都被定义了,我想得到它们的最大值,或者如果只定义了一个,我想得到它们的最大值,如果定义了 none,则 None

如何在第一个 Optionmap() 中不涉及大量 isDefined 检查的情况下优雅地编写此代码?

通常,如果两个 值都已定义,您会想要做一些事情。 在这种情况下,您可以使用 for-comprehension:

val aOpt: Option[Int] = getIntOpt
val bOpt: Option[Int] = getIntOpt

val maxOpt: Option[Int] = 
    for {
        a <- aOpt
        b <- bOpt
    } yield max(a, b)

现在,您描述的问题并不常见。如果两个值都已定义,您想做某事,但如果只定义其中一个,您还想检索选项的值。

我只会使用上面的 for-comprehension,然后在 maxOpt 结果为 None 时,将两个调用链接到 orElse 以提供替代值。

maxOpt orElse aOpt orElse bOpt

orElse的签名:

def orElse[B >: A](alternative: ⇒ Option[B]): Option[B]

你可以这样使用:

def optMax(op1:Option[Int], op2: Option[Int]) = op1 ++ op2 match {    
  case Nil => None  
  case list => list.max
}

或者更好的一个:

def f(vars: Option[Int]*) = (for( vs <- vars) yield vs).max

@jwvh,感谢您的改进:

def f(vars: Option[Int]*) = vars.max

模式匹配会让一些东西容易掌握,但这可能不是最优雅的方式:

def maxOpt[T](optA: Option[T], optB: Option[T])(implicit f: (T, T) => T): Option[T] = (optA, optB) match {
    case (Some(a), Some(b)) => Some(f(a, b))
    case (None, Some(b)) => Some(b)
    case (Some(a), None) => Some(a)
    case (None, None) => None
}

你最终会得到这样的结果:

scala> maxOpt(Some(1), None)(Math.max)
res2: Option[Int] = Some(1)

一旦你有了那个建筑,块,你就可以在 for-comp 或 monadic 操作中使用它。

这是另一个 fwiw:

import scala.util.Try
def maxOpt (a:Option[Int]*)= Try(a.flatten.max).toOption

它适用于 n 个参数(包括零个参数)。

要获得 maxOpt,您还可以使用应用程序,使用 Scalaz 看起来像 (aOpt |@| bOpt) { max(_, _) } & 然后按照 @dcastro 的建议链接 orElses。

我假设您期望结果是 Some[Int]|None,而不是 Int|None(否则 return 类型必须是 Any):

  def maxOption(opts: Option[Int]*) = {
    val flattened = opts.flatten
    flattened.headOption.map { _ => flattened.max }
  }

实际上,Scala已经或多或少地直接给了你这种能力。

scala> import Ordering.Implicits._
import Ordering.Implicits._

scala> val (a,b,n:Option[Int]) = (Option(4), Option(9), None)
a: Option[Int] = Some(4)
b: Option[Int] = Some(9)
n: Option[Int] = None

scala> a max b
res60: Option[Int] = Some(9)

scala> a max n
res61: Option[Int] = Some(4)

scala> n max b
res62: Option[Int] = Some(9)

scala> n max n
res63: Option[Int] = None

一个 Haskell-ish 对这个问题的看法是观察以下操作:

max, min :: Ord a => a -> a -> a
max a b = if a < b then b else a
min a b = if a < b then a else b

...关联:

max a (max b c) == max (max a b) c
min a (min b c) == min (min a b) c

因此,任何类型 Ord a => a 与这些操作中的任何一个一起都是一个 semigroup,一个可以构建可重用抽象的概念。

并且您正在处理 Maybe(Haskell 代表 "option"),它向基础 a 类型添加了一个通用 "neutral" 元素(您希望 max Nothing x == x 成为一项法律)。这将带您进入 monoids,它是半群的一个子类型。

Haskell semigroups library provides a Semigroup type class and two wrapper types, Max and Min,一般实现相应的行为。

由于我们正在处理 Maybe,就该库而言,捕获您想要的语义的类型是 Option (Max a)——一个与 [=19 具有相同二进制操作的幺半群=] semigroup,并使用 Nothing 作为标识元素。那么函数就变成了:

maxOpt :: Ord a => Option (Max a) -> Option (Max a) -> Option (Max a)
maxOpt a b = a <> b

...因为它只是 Option (Max a)<> 运算符,所以不值得写。您还获得了所有其他效用函数和 类 适用于 SemigroupMonoid,因此例如要查找 [Option (Max a)] 的最大元素,您只需使用 the mconcat function.

scalaz 库带有实现这些特性的 Semigroup and a Monoid trait, as well as Max, Min, MaxVal and MinVal tags,所以实际上我在 Haskell 中演示的内容也存在于 scalaz 中。