【问题标题】:Min/max with Option[T] for possibly empty Seq?对于可能为空的 Seq,使用 Option[T] 的最小值/最大值?
【发布时间】:2012-06-06 21:02:20
【问题描述】:

我正在做一些 Scala 体操,其中有 Seq[T],我试图在其中找到“最小”元素。这就是我现在所做的:

val leastOrNone = seq.reduceOption { (best, current) =>
    if (current.something < best.something) current
    else best
}

它运行良好,但我不太满意 - 这么简单的东西有点长,I don't care much for "if"s。使用minBy 会更优雅:

val least = seq.minBy(_.something)

... 但是当序列为空时,minminBy 会抛出异常。有没有一种惯用的、更优雅的方式来查找可能为空的列表中的最小元素为Option

【问题讨论】:

    标签: scala scala-collections


    【解决方案1】:
    seq.reduceOption(_ min _)
    

    做你想做的事?


    编辑:这是一个包含您的 _.something 的示例:

    case class Foo(a: Int, b: Int)
    val seq = Seq(Foo(1,1),Foo(2,0),Foo(0,3))
    val ord = Ordering.by((_: Foo).b)
    seq.reduceOption(ord.min)  //Option[Foo] = Some(Foo(2,0))
    

    或者,作为通用方法:

    def minOptionBy[A, B: Ordering](seq: Seq[A])(f: A => B) = 
      seq reduceOption Ordering.by(f).min
    

    你可以用minOptionBy(seq)(_.something)调用它

    【讨论】:

    • 也可以使用seq reduceOption math.min。这更有效,因为它不需要隐式转换。
    • @Antoras 想法不错,但我相信任何差异在实践中都会被优化掉 - 至少,这就是我的微基准测试所显示的
    【解决方案2】:

    Scala 2.13 开始,minByOption/maxByOption 现在是标准库的一部分,如果序列为空,则返回 None

    seq.minByOption(_.something)
    
    List((3, 'a'), (1, 'b'), (5, 'c')).minByOption(_._1) // Option[(Int, Char)] = Some((1,b))
    List[(Int, Char)]().minByOption(_._1)                // Option[(Int, Char)] = None
    

    【讨论】:

      【解决方案3】:

      带有 Scalaz 的安全、紧凑和 O(n) 版本:

      xs.nonEmpty option xs.minBy(_.foo)
      

      【讨论】:

        【解决方案4】:

        由于O(nlogn) 的复杂性,几乎无法选择任何更大的列表:

        seq.sortBy(_.something).headOption
        

        【讨论】:

        • 打破了奥卡姆的剃刀。它的碎片锋利而危险!问题是安全地找到最大值,而不是对列表进行排序。
        • @fatuhoku 从数学(因此是纯 FP?)的角度来看,sortBy + headOption 似乎等同于 minByOpt — 为什么它打破了奥卡姆剃刀?
        • @EricAllik 嗯。不知道我为什么写那个。几年前我显然不知道奥卡姆剃刀实际上是什么。可耻!现在我对这样的答案有了更多的欣赏,尽管接受的答案是一个更好的主意。
        【解决方案5】:

        也可以这样做

        Some(seq).filter(_.nonEmpty).map(_.minBy(_.something))
        

        【讨论】:

          【解决方案6】:

          这个怎么样?

          import util.control.Exception._
          allCatch opt seq.minBy(_.something)
          

          或者,更详细一点,如果您不想吞下其他异常:

          catching(classOf[UnsupportedOperationException]) opt seq.minBy(_.something)
          

          或者,您可以使用以下方式对所有集合进行拉皮条:

          import collection._
          
          class TraversableOnceExt[CC, A](coll: CC, asTraversable: CC => TraversableOnce[A]) {
          
            def minOption(implicit cmp: Ordering[A]): Option[A] = {
              val trav = asTraversable(coll)
              if (trav.isEmpty) None
              else Some(trav.min)
            }
          
            def minOptionBy[B](f: A => B)(implicit cmp: Ordering[B]): Option[A] = {
              val trav = asTraversable(coll)
              if (trav.isEmpty) None
              else Some(trav.minBy(f))
            }
          }
          
          implicit def extendTraversable[A, C[A] <: TraversableOnce[A]](coll: C[A]): TraversableOnceExt[C[A], A] =
            new TraversableOnceExt[C[A], A](coll, identity)
          
          implicit def extendStringTraversable(string: String): TraversableOnceExt[String, Char] =
            new TraversableOnceExt[String, Char](string, implicitly)
          
          implicit def extendArrayTraversable[A](array: Array[A]): TraversableOnceExt[Array[A], A] =
            new TraversableOnceExt[Array[A], A](array, implicitly)
          

          然后写seq.minOptionBy(_.something)

          【讨论】:

          • -1 相当于一个空的 catch 块。抛出的任何其他异常也将被吞没。
          【解决方案7】:

          我之前也有同样的问题,所以我扩展了 Ordered 并实现了比较功能。 这是一个例子:

           case class Point(longitude0: String, latitude0: String)  extends Ordered [Point]{
          
            def this(point: Point) = this(point.original_longitude,point.original_latitude)
            val original_longitude = longitude0
            val original_latitude = latitude0
          
            val longitude = parseDouble(longitude0).get 
            val latitude = parseDouble(latitude0).get  
          
            override def toString: String = "longitude: " +original_longitude +", latitude: "+ original_latitude
          
            def parseDouble(s: String):  Option[Double] = try { Some(s.toDouble) } catch { case _ => None }
          
            def distance(other: Point): Double =
              sqrt(pow(longitude - other.longitude, 2) + pow(latitude - other.latitude, 2))
          
           override def compare(that: Point): Int = {
            if (longitude < that.longitude)
              return -1
            else if (longitude == that.longitude && latitude < that.latitude)
              return -1
            else
              return 1
           }
          }
          

          所以如果我有一个点序列 我可以要求 max 或 min 方法

            var points =  Seq[Point]()
          
          val maxPoint = points.max
          val minPoint = points.min
          

          【讨论】:

          • 如何解决空集合的 min 和 max 抛出异常的问题?
          【解决方案8】:

          你总是可以这样做:

          case class Foo(num: Int)
          
          val foos: Seq[Foo] = Seq(Foo(1), Foo(2), Foo(3))
          val noFoos: Seq[Foo] = Seq.empty
          
          def minByOpt(foos: Seq[Foo]): Option[Foo] =
            foos.foldLeft(None: Option[Foo]) { (acc, elem) => 
              Option((elem +: acc.toSeq).minBy(_.num)) 
            }
          

          然后使用like:

          scala> minByOpt(foos)
          res0: Option[Foo] = Some(Foo(1))
          
          scala> minByOpt(noFoos)
          res1: Option[Foo] = None
          

          【讨论】:

            【解决方案9】:

            对于 scala

            Try(seq.minBy(_.something)).toOption
            

            对于 Scala 2.13

            seq.minByOption(_.something)
            

            【讨论】:

              【解决方案10】:

              在 Haskell 中,您可以将 minimumBy 调用包装为

              least f x | Seq.null x = Nothing
                        | otherwise  = Just (Seq.minimumBy f x) 
              

              【讨论】:

              • 我会首先避免异常——使用警卫来排除异常发生的机会。这在语义上比依靠异常语义给你一个可选值更简洁。
              • Haskell 很酷,毫无疑问。但是“这就是你可以用另一种编程语言做到的方式”并不能回答这个问题。
              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2018-02-11
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2013-09-13
              相关资源
              最近更新 更多