【问题标题】:Functional composition of different types of Tasks - Scala不同类型任务的功能组合 - Scala
【发布时间】:2019-03-12 15:04:38
【问题描述】:

目前,我正在设计一个在 Scala 中构建通用管道的设计(纯粹用于学习目的)。为此,我从一个基本结构 Task 开始,它需要一些 TaskConfiguration(现在,我们可以假设这个 TaskConfiguration 是一个特定于 Task 功能的案例类)。特征结构如下:

trait Task[T <: TaskConfiguration] {
  type Out

  def taskConfiguration: T
  def execute(previousOutput: Option[Out]): Option[Out]
}

要求: 1. 我可以有多个扩展任务特征的任务。比如,ReadTask、WriteTask 等, 2. 每个任务都有自己的“out”类型

我的问题是:给定一个 List[Task],我如何编写方法调用来执行。尝试了多种方法来组合它们,但我一直遇到问题,即我无法区分以前的任务和当前任务的输出,因为我只有一个类型成员来指定此任务可以处理的内容。

我希望我们可以用 Scala 解决这个问题。但考虑到我对使用 Scala 进行函数式编程还很陌生,我想不通。非常感谢您。

注意:对这个问题的描述可能有点脱离上下文。但这是我能想到的最好的,在这个时候。如果您认为我们可以做得更好,请随意编辑。如果您觉得这个问题没有任何意义,请在 cmets 中提出您的想法,以便我处理。

【问题讨论】:

  • WDYM 通过“组合方法调用来执行”?
  • @YuvalItzchakov 这里我们正在处理 List[Task] 并且需要在每个 Task 上依次调用 execute 方法。就像Task1的输出应该输入到Task2等等。例如,如果管道就像复制文件,那么相应的任务将是 ReadTask、WriteTask。 ReadTask 将从文件中读取数据并将这些行提供给 WriteTask,WriteTask 进而写入不同的文件。如果您需要更多详细信息,请告诉我。

标签: scala function-composition


【解决方案1】:

您可以使用类似于 Scala 函数中的andThen 的模式。

我编译了一个小例子:


import scala.util.{Try, Success, Failure}

type TaskConfiguration = Any

trait Task[-C <: TaskConfiguration, +O <: TaskConfiguration] {

  def execute(configuration: C): Option[O]

  def andThen[O2 <: TaskConfiguration](secondTask: Task[O, O2]): Task[C, O2] = {
    val firstTask = this

    new Task[C, O2] {
       def execute(configuration: C): Option[O2] =
         firstTask.execute(configuration).flatMap(secondTask.execute(_))
    }
  }
}

// From here on it's the example!

case class UnparsedNumber(value: String)

trait ParsedNumber {
  val value: Int
}

case class ParsedPositiveNumber(int: Int) extends ParsedNumber {
  val value: Int = int
}

case class HumanReadableNumber(value: String)


val task1 = new Task[UnparsedNumber, ParsedPositiveNumber] {
  def execute(configuration: UnparsedNumber): Option[ParsedPositiveNumber] = {
    Try(configuration.value.toInt) match {
      case Success(i) if i >= 0 => Some(ParsedPositiveNumber(i))
      case Success(_) => None
      case Failure(_) => None
    }
  }
}

val task2 = new Task[ParsedNumber, HumanReadableNumber] {
  def execute(configuration: ParsedNumber): Option[HumanReadableNumber] = {
    if(configuration.value < 1000 && configuration.value > -1000)
      Some(HumanReadableNumber(s"The number is $configuration"))
    else
      None
  }
}

val combined = task1.andThen(task2)

println(combined.execute(UnparsedNumber("12")))
println(combined.execute(UnparsedNumber("12x")))
println(combined.execute(UnparsedNumber("-12")))
println(combined.execute(UnparsedNumber("10000")))
println(combined.execute(UnparsedNumber("-10000")))

Try it out!


编辑:

关于您的 cmets,这种方法可能更符合您的要求:

case class Task[-C, +O](f: C => Option[O]) {

  def execute(c: C): Option[O] = f.apply(c)
}

case class TaskChain[C, O <: C](tasks: List[Task[C, O]]) {

  def run(initial: C): Option[O] = {

    def runTasks(output: Option[C], tail: List[Task[C, O]]): Option[O] = {
      output match {
        case Some(o) => tail match {
          case head :: Nil => head.execute(o)
          case head :: tail => runTasks(head.execute(o), tail)
          case Nil => ??? // This should never happen!
        }
        case None => None
      }
    }

    runTasks(Some(initial), tasks)
  }
}

// Example below:

val t1: Task[Int, Int] = Task(i => Some(i * 2))
val t2: Task[Int, Int] = Task(i => Some(i - 100))
val t3: Task[Int, Int] = Task(i => if(i > 0) Some(i) else None)


val chain: TaskChain[Int, Int] = TaskChain(List(t1, t2, t3))

println(chain.run(100))
println(chain.run(10))

Try it out!

引用:

您需要了解的是,如果您将Tasks 打包在List[Task] 中并将其用作Tasks 的链,则输出必须至少是输入的子类型。 C &lt;: TaskConfigurationO &lt;: C 导致:O &lt;: C &lt;: TaskConfiguration 这也意味着 O &lt;: TaskConfiguration


如果您不理解其中的任何部分,我很乐意进一步解释。

我希望这会有所帮助。

【讨论】:

  • 感谢您的快速回复。看来,我的要求有点不同。 “O”不是TaskConfiguration 的子类型。这应该是一个类型成员(需要被 Trait 的子类覆盖)并表示“执行”方法的输出。并且“执行”将先前的“任务的执行方法的输出”作为输入。请参阅我的评论,以回应 Yuval 的问题。如有错误请指正。
  • @Krishna 看,您需要了解的是,如果您将 Tasks 打包到 List[Task] 中并将其用作 Tasks 的链,则输出 有至少是输入的一个子类型。 C &lt;: TaskConfigurationO &lt;: C 导致:O &lt;: C &lt;: TaskConfiguration 这也意味着 O &lt;: TaskConfiguration
  • @Krishna 看看这个例子:scalafiddle.io/sf/lwkSNEc/8
  • 按照上面的评论改变了我的特质,它就像一个魅力。非常感谢。
  • @Krishna 乐于助人!
【解决方案2】:

我建议看看cats and free monads 可以为您提供什么。按照这种方法,我将开始定义用于定义管道程序的 ADT。类似的东西:

trait TaskE[Effect]
case class ReadTask[Input, SourceConfig](source: SourceConfig) extends TaskE[Input]
case class WriteTask[Output, SinkConfig](out: Output, sink: SinkConfig) extends TaskE[Unit]

然后应用 Free monads(如上面链接中所述)来定义您的管道流。比如:

val pipeline: Task[Unit] = 
  for {
    input1 <- read(source1)
    input2 <- read(source2)
    _      <- write(input1 + input2, sink1)
  } yield ()

现在它将取决于编译器(这是一个自然的转换,描述了如何从Task[A] 转换为F[A],而F 可能是IdTryFuture,... ) 你定义这个程序将如何运行:

val myCompiler: Task ~> Id = ???
val tryCompiler: Task ~> Try = ???

pipeline.foldMap(myCompiler)  // Id[Unit]
pipeline.foldMap(tryCompiler) // Try[Unit]

您可以拥有任意数量的“编译器”,这并不意味着更改您的管道(“程序”)定义。

【讨论】:

  • @Santos 免费单子似乎是一个有趣的概念。但是,您的回复并未涉及作文部分。在给定的上下文中,我们将有一个 List[Task] 需要组合,这与 Pipeline 示例不同,在该示例中,我事先知道存在多少任务及其执行顺序。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-05-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多