Функциональная композиция разных типов задач - Scala - PullRequest
3 голосов
/ 12 марта 2019

В настоящее время я работаю над проектом по созданию общего конвейера в Scala (исключительно для целей обучения). Для этого я начал с базовой конструкции Task, которая принимает некоторую TaskConfiguration (сейчас мы можем предположить, что эта TaskConfiguration является классом case, специфичным для функциональных возможностей 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, я не мог этого понять. Заранее большое спасибо.

Примечание : Описание этого вопроса может показаться немного неконтролируемым. Но это лучшее, что я мог придумать, на данный момент. Не стесняйтесь редактировать это, если вы думаете, что мы могли бы сделать это лучше. Если вы думаете, что этот вопрос не имеет никакого смысла, пожалуйста, укажите его в комментариях с вашими мыслями, чтобы я мог позаботиться об этом.

Ответы [ 2 ]

2 голосов
/ 12 марта 2019

Вы можете использовать шаблон, аналогичный andThen из функций Scala.

Я собрал небольшой пример:


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")))

Попробуйте!


Изменить:

Что касается ваших комментариев, этот подход может быть больше, чем вы ищете:

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))

Попробуйте!

Цитата:

Что вам нужно понять, так это то, что если вы упакуете Task s в List[Task] и используете его как цепочку Task s, выходные данные должны быть как минимум подтипом ввода. C <: TaskConfiguration и O <: C приводит к: O <: C <: TaskConfiguration, что также означает O <: TaskConfiguration.


Если вы ничего не поняли, я с удовольствием объясню.

Надеюсь, это поможет.

0 голосов
/ 13 марта 2019

Я бы посоветовал взглянуть на то, что кошки и бесплатные монады могут предложить вам. Следуя этому подходу, я бы начал определять 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]

И затем примените Свободные монады (как упомянуто в ссылке выше) для определения вашего конвейерного потока. Что-то вроде:

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

Теперь это будет зависеть от компилятора (это естественное преобразование, которое описывает, как преобразовать из Task[A] в F[A], а F может быть Id, Try, Future, ... ) вы определяете, как будет работать эта программа:

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

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

У вас может быть столько «компиляторов», сколько вы хотите, и это не означает, что нужно изменить определение вашего конвейера («программы»).

...