【问题标题】:Scala - evaluate function calls sequentially until one returnScala - 按顺序评估函数调用,直到一个返回
【发布时间】:2018-01-05 20:30:15
【问题描述】:

我有一些“旧”端点可以返回我正在寻找的数据。

def mainCall(id): Data {

    maybeMyDataInEndpoint1(id: UUID): DataA

    maybeMyDataInEndpoint2(id: UUID): DataB

    maybeMyDataInEndpoint3(id: UUID): DataC
}
  • 如果没有找到DataX,则可以返回null
  • 每种方法的返回类型都不同。有一个convert 方法可以将每个DataX 转换为统一的Data
  • 端点不是 Scala 风格的

什么是最佳 Scala 方法来按顺序评估这些方法调用,直到获得所需的值?

在伪中,我会做类似的事情:

val myData = maybeMyDataInEndpoint1 getOrElse maybeMyDataInEndpoint2 getOrElse maybeMyDataInEndpoint3

【问题讨论】:

  • 如果您的任何调用可能返回null,为什么不使用Option 来帮助排序处理? 3 种不同的 DataX 是否有 3 种不同的 convert 方法,或者单个 convert 是否适用于不同的 DataX 类型?
  • “没有选项可用于包装...”是什么意思???您在询问“最好的 scala 方法”。 Option IS scala ...
  • @Dima 你绝对正确。对不起,我的措辞不好。当然 Option 是 Scala :) 我的意思是端点不能返回 Option。这就是为什么我稍后在伪...中使用getOrElse
  • Option(function) 那么呢?
  • @Dima 还不够

标签: scala


【解决方案1】:

我会使用更简单的方法,尽管其他答案使用更复杂的语言功能。 只需使用Option() 捕获null,与orElse 链接。我假设方法 convertX(d:DataX):Data 用于显式转换。因为它可能根本找不到,所以我们返回一个Option

def mainCall(id: UUID): Option[Data] {
  Option(maybeMyDataInEndpoint1(id)).map(convertA)
  .orElse(Option(maybeMyDataInEndpoint2(id)).map(convertB))
  .orElse(Option(maybeMyDataInEndpoint3(id)).map(convertC))
}

【讨论】:

  • 看起来是最简单方便的解决方案。之前觉得还不够。这个有什么缺点吗?
【解决方案2】:

也许您可以将这些方法提升为Lists 和collectFirst 的高阶函数,例如:

  val fs = List(maybeMyDataInEndpoint1 _, maybeMyDataInEndpoint2 _, maybeMyDataInEndpoint3 _)

  val f = (a: UUID) => fs.collectFirst {
    case u  if u(a) != null => u(a)
  }
  r(myUUID)

【讨论】:

  • 忘了说,每个端点的返回类型都不一样。后面还有convertTo统一类型...编辑q。谢谢。
  • 如果对于不同的返回类型,也许你可以将val fs更新为val fs: List[UUID => Any],它会将返回类型擦除为Any
【解决方案3】:

最好的 Scala 方法恕我直言,以最直接的方式做事。

  • 要处理可选值(或来自 Java 领域的 nulls),请使用 Option
  • 要按顺序评估方法列表,请折叠Seq 的函数。
  • 要将一种数据类型转换为另一种数据类型,请根据情况和您的偏好使用 (1.) 隐式转换或 (2.) 常规函数。

    1. 编辑)假设隐式转换:

      def legacyEndpoint[A](endpoint: UUID => A)(implicit convert: A => Data) =
        (id: UUID) => Option(endpoint(id)).map(convert)
      
      val legacyEndpoints = Seq(
        legacyEndpoint(maybeMyDataInEndpoint1),
        legacyEndpoint(maybeMyDataInEndpoint2),
        legacyEndpoint(maybeMyDataInEndpoint3)
      )
      
      def mainCall(id: UUID): Option[Data] =
        legacyEndpoints.foldLeft(Option.empty[Data])(_ orElse _(id))
      
    2. 编辑)使用显式转换:

      def legacyEndpoint[A](endpoint: UUID => A)(convert: A => Data) =
        (id: UUID) => Option(endpoint(id)).map(convert)
      
      val legacyEndpoints = Seq(
        legacyEndpoint(maybeMyDataInEndpoint1)(fromDataA),
        legacyEndpoint(maybeMyDataInEndpoint2)(fromDataB),
        legacyEndpoint(maybeMyDataInEndpoint3)(fromDataC)
      )
      
      ... // same as before
      

【讨论】:

  • 这是最干净的恕我直言,除了 foldLeft 在我们得到结果后继续,而不是提前停止迭代(即使 endPoints 没有执行)。
  • 是的,我同意。还有另一个答案建议使用collectFirst,但是它会执行两次端点,这可能会更糟。
  • 另一个更好的选择可能是使用legacyEndpoints.iterator.map(_(id)).find(_.isDefined).flatten
  • 谢谢,很干净。我有点担心foldLeft 会执行所有端点,对吧?
  • 不,它不会因为orElse 的参数(即端点调用_(id))是按名称命名的。仅当左侧的累加器为None 时才会执行。但是,它将为每个端点创建一个(未评估的)thunk。如果你想避免这种情况,你可以使用iterator 版本。
【解决方案4】:

这是一种方法。

(1) 为方便起见,您可以将 convert 方法设为隐式(或将它们包装到隐式包装器中)。

(2) 然后使用Stream 从方法调用中构建chain。你应该给类型推断一个提示,你希望你的流包含 Data 元素(不是旧方法返回的 DataX),以便将适当的隐式 convert 应用于旧方法调用的每个结果。

(3) 因为Stream 是惰性的,并且“按名称”评估它的尾部,所以到目前为止只有第一个方法被调用。此时您可以应用惰性过滤器跳过null 结果。

(4) 现在您可以实际评估chain,使用headOption 获得第一个非null 结果

(HACK) 不幸的是,scala 类型推断(在撰写本文时为 v2.12.4)还不够强大,无法使用 #:: 流方法,除非您指导它的每一步方法。使用cons 可以使推理愉快,但很麻烦。此外,使用伴随对象的 vararg apply 方法构建流也不是一个选项,因为 scala 还不支持“按名称”可变参数。在下面的示例中,我使用了 streamtoLazyData 方法的组合。 stream 是一个通用助手,从 0-arg 函数构建流。 toLazyData 是一种隐式“按名称”转换,旨在与从 DataX 转换为 Data 的隐式 convert 函数相互作用。

这里是更详细地演示了这个想法的演示:

object Demo {

  case class Data(value: String)
  class DataA
  class DataB
  class DataC

  def maybeMyDataInEndpoint1(id: String): DataA = {
    println("maybeMyDataInEndpoint1")
    null
  }

  def maybeMyDataInEndpoint2(id: String): DataB = {
    println("maybeMyDataInEndpoint2")
    new DataB
  }

  def maybeMyDataInEndpoint3(id: String): DataC = {
    println("maybeMyDataInEndpoint3")
    new DataC
  }

  implicit def convert(data: DataA): Data = if (data == null) null else Data(data.toString)
  implicit def convert(data: DataB): Data = if (data == null) null else Data(data.toString)
  implicit def convert(data: DataC): Data = if (data == null) null else Data(data.toString)

  implicit def toLazyData[T](value: => T)(implicit convert: T => Data): (() => Data) = () => convert(value)

  def stream[T](xs: (() => T)*): Stream[T] = {
    xs.toStream.map(_())
  }

  def main (args: Array[String]) {

    val chain = stream(
      maybeMyDataInEndpoint1("1"),
      maybeMyDataInEndpoint2("2"),
      maybeMyDataInEndpoint3("3")
    )

    val result = chain.filter(_ != null).headOption.getOrElse(Data("default"))

    println(result)

  }

}

打印出来:

maybeMyDataInEndpoint1
maybeMyDataInEndpoint2
Data(Demo$DataB@16022d9d)

这里maybeMyDataInEndpoint1 返回nullmaybeMyDataInEndpoint2 需要被调用,传递DataBmaybeMyDataInEndpoint3 永远不会被调用,因为我们已经有了结果。

【讨论】:

    【解决方案5】:

    我认为@g.krastev 的回答非常适合您的用例,您应该接受这一点。我只是在上面花费了一些时间来展示如何使用cats 使最后一步变得更好。

    首先,样板:

    import java.util.UUID
    
    final case class DataA(i: Int)
    final case class DataB(i: Int)
    final case class DataC(i: Int)
    type Data = Int
    
    def convertA(a: DataA): Data = a.i
    def convertB(b: DataB): Data = b.i
    def convertC(c: DataC): Data = c.i
    
    def maybeMyDataInEndpoint1(id: UUID): DataA = DataA(1)
    def maybeMyDataInEndpoint2(id: UUID): DataB = DataB(2)
    def maybeMyDataInEndpoint3(id: UUID): DataC = DataC(3)
    

    这基本上就是您所拥有的,您可以在 REPL 中复制/粘贴并进行编译。

    现在,让我们首先声明一种将您的每个端点变成安全统一的方法:

    def makeSafe[A, B](evaluate: UUID ⇒ A, f: A ⇒ B): UUID ⇒ Option[B] = 
      id ⇒ Option(evaluate(id)).map(f)
    

    有了这个,例如,您可以调用以下命令将maybeMyDataInEndpoint1 转换为UUID => Option[A]

    makeSafe(maybeMyDataInEndpoint1, convertA)
    

    现在的想法是将您的端点变成UUID => Option[A] 的列表并折叠该列表。这是您的清单:

    val endpoints = List(
      makeSafe(maybeMyDataInEndpoint1, convertA),
      makeSafe(maybeMyDataInEndpoint2, convertB),
      makeSafe(maybeMyDataInEndpoint3, convertC)
    )
    

    您现在可以手动折叠它,这就是 @g.krastev 所做的:

    def mainCall(id: UUID): Option[Data] = 
      endpoints.foldLeft(None: Option[Data])(_ orElse _(id))
    

    如果您对 cats 依赖项感到满意,折叠选项列表的概念只是常见模式的具体用例(FoldableMonoid 的交互):

    import cats._
    import cats.implicits._
    
    def mainCall(id: UUID): Option[Data] = endpoints.foldMap(_(id))
    

    还有其他方法可以让它变得更好,但在这种情况下它们可能有点矫枉过正——我可能会声明一个类型类来将任何类型转换为Data,例如,给makeSafe 一个更干净的类型签名。

    【讨论】:

    • 不是短路了吗?我得仔细检查一下,谢谢指出!
    猜你喜欢
    • 1970-01-01
    • 2018-11-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多