Как элегантно реализовать паттерн конвейера с помощью Scala - PullRequest
8 голосов
/ 07 марта 2012

Я хочу построить шаблон конвейера с помощью Scala.Я хотел бы после того, как я напишу объекты конвейера, они могут быть соединены вместе следующим образом:

Pipeline1 :: Pipeline2 :: Pipeline3 ...

Я до сих пор экспериментировал с несколькими идеями.Некоторые работают, а некоторые нет.Но ни один из них, кажется, полностью не избавился от стандартного кода.Ниже приведено следующее.

Сначала определите абстрактный класс Pipeline и Source:

// I is the input type and O is the output type of the pipeline
abstract class Pipeline[I, +O](p: Pipeline[_, _ <: I]) {

  val source = p
  val name: String
  def produce(): O
  def stats():String
}
abstract class Source[+T] extends Pipeline[AnyRef, T](null)

Затем я создал два конвейера и попытался связать их вместе

// this creates a random integer
class RandomInteger extends Source[Int] {
  override val name = "randInt"

  def produce() = {
    scala.Math.round(scala.Math.random.asInstanceOf[Float] * 10)
  }

  def stats()="this pipeline is stateless"
}

// multiply it by ten
class TimesTen(p: Pipeline[_, Int]) extends Pipeline[Int, Int](p) {
  private var count = 0 // this is a simple state of the pipeline
  override val name = "Times"
  def produce = {
    val i = source.produce()
    count += 1 // updating the state
    i * 10
  }
  def stats() = "this pipeline has been called for " + count + " times"
}

object TimesTen {
  // this code achieves the desired connection using ::
  // but this has to be repeated in each pipeline subclass. 
  // how to remove or abstract away this boilerplate code? 
  def ::(that: Pipeline[_, Int]) = new TimesTen(that)
}

Это основной класс, в котором связаны два конвейера.

object Pipeline {
  def main(args: Array[String]) {
    val p = new RandomInteger() :: TimesTen
    println(p.source)
    for (i <- 0 to 10)
      println(p.produce())
    println(p.stats())
  }
}

Так что этот код работает.Но мне пришлось бы повторять код в сопутствующем объекте TimesTen в каждом написанном мной классе конвейера.Это конечно не желательно.Есть ли лучший способ сделать это?Отражение может сработать, но я слышал о нем плохие вещи, например, что-либо с отражением - плохой дизайнЯ также не уверен насчет поддержки отражений в Scala.

Спасибо за ваше время.

Обновление : я разработал эту игрушечную задачу, чтобы ее было легче понять.Как общее решение, и как требует мое приложение, каждый объект конвейера имеет состояние, которое идеально инкапсулируется в самом объекте, а не подвергается воздействию любого другого конвейера.Я изменил код выше, чтобы отразить это.Хотелось бы, чтобы могло быть объектно-ориентированное решение.Я все еще экспериментирую и сообщу вам, если я найду один.

Обновление 2 : После некоторых мыслей, я думаю, что идея конвейера на самом деле является просто обобщенной функцией, которая содержит некоторыевнутренние состояния, а также способность составлять функцию Function0 с функцией Function1.В Scala класс Function0 не имеет метода compose() или andThen().

Ответы [ 4 ]

8 голосов
/ 07 марта 2012

Если я что-то упустил, ваши объекты конвейера - это просто функции, а ваш оператор :: просто "составляет"

val randomInteger: ()=>Int = () => scala.Math.round(scala.Math.random.asInstanceOf[Float] * 10)
val timesTen :Int => Int = x => x*10    
val pipeline: () =>Int = timesTen compose randomInteger

Ваш метод "yield ()" просто "apply ()", но обычно используется сокращение от "()". Небольшое количество библиотечных pimping позволит вам использовать оператор для композиции. Это один из тех случаев, когда объектно-ориентированный шаблон действительно мешает простым функциональным концепциям. К счастью, Scala позволяет вам избегать шаблонов для многих подобных случаев.

7 голосов
/ 07 марта 2012

Вот решение с объектами, использующими andThen. Идея состоит в том, чтобы форсировать создание Function1 объектов с помощью ввода Unit. Соединение двух конвейеров создает новый конвейер с двумя функциями вместе. Это решение позволяет трубопроводам иметь внутренние состояния.

Еще одним упрощением будет использование apply() вместо produce(). Это оставлено в качестве упражнения для читателя.

abstract class Pipeline[-I, +O] {

  val name: String
  def produce : I => O
  def stats(): String

  def ->[X](seg:Pipeline[_ >: O, X]):Pipeline[I, X] = {
    val func = this.produce
    val outerName = this.name
    new Pipeline[I, X] {
      val name = outerName + "." + seg.name
      def produce = func andThen seg.produce 
      def stats = seg.stats
    }
  }
}

abstract class Source[+T] extends Pipeline[Unit, T] {
}

class RandomInteger extends Source[Int] {
  override val name = "randInt"
  def produce: Unit => Int = (x:Unit) => scala.Math.round(scala.Math.random.asInstanceOf[Float] * 10) 
  def stats() = "stateless"
}

class TimesTen() extends Pipeline[Int, Int] {
  private var count = 0
  override val name = "times"
  def produce : Int => Int = (x:Int) => {    
    count += 1
    x * 10
  }
  def stats() = "called for " + count + " times"
}


object Main {
  def main(args: Array[String]) {
    val p = new RandomInteger() -> new TimesTen() 

    for (i <- 0 to 10)
      println(p.produce())
    println(p.name)    // print "randInt.times"
    println(p.stats()) // print "called for 11 times"
  }
}
3 голосов
/ 24 марта 2013
object Pipelining {  implicit def toPipe[T](x : T) = new {     def :: [U](f : T => U) = f(x)  }}

import Pipelining._
List(2,3,4) :: (_.map(_*3)) :: (_.map(_.toString)) :: println 

все кредиты для оператора StephaneLD "|> как в F #"

http://www.scala -lang.org / node / 8747

0 голосов
/ 08 марта 2012

Вы имеете в виду как Поток данных или Функциональное Реактивное Программирование?Попробуйте этот вопрос .Активно развивается реактивная библиотека - об остальном я не знаю.

...