8

I am trying to define a set of "LazyChains" that will be used to process incoming messages at a future time. I want the API of the LazyChains to be indistinguishable from the Scala collections APIs (ie: Seq, Stream, etc). This would allow me to declaratively define filtering/transformations/actions ahead of time before the messages arrive. It's possible that this is a well known pattern that I don't know the name of, so that is making it hard for me to find any results on this.

This is an example of what I'm trying to accomplish:

val chainA = LazyChain()
 .filter(_ > 1)
 .map(x => x * 2)
 .foreach(println _)

val chainB = LazyChain()
 .filter(_ > 5)
 .flatMap(x => Seq(x, x))
 .foreach(println _)

 chainA.apply(2)  // should print "4"
 chainA.apply(1)  // should print nothing
 chainB.apply(10) // should print "10" - twice

Does this pattern already exist in the Scala collections API? If not, how can I implement this class LazyChain?

This is my current attempt at this. I can't seem to work out how to get the types to work:

case class LazyChain[I, O](val filter : Option[I => Boolean],
                      val transform : I => O,
                      val action : Option[O => Unit]) {

  def filter(otherFilter : I => Boolean): LazyChain[I, O]  = {
      val newFilter = Some({ x : I => {
        filter.map(_.apply(x)).getOrElse(true) && otherFilter.apply(x)
      }})
      copy(filter = newFilter)
  }

  def map[X](otherTransform : O => X) : LazyChain[I, X] = {
    new LazyChain[I, X](
      filter = filter,
      transform = (x: I) => {
        otherTransform.apply(transform.apply(x))
      },
      /*
        type mismatch;
        [error]  found   : Option[O => Unit]
        [error]  required: Option[X => Unit]
      */
      action = action 
    )
  }

  def flatMap[X](otherTransform : O => Seq[X]) : LazyChain[I, X] = {
    new LazyChain[I, X](
      filter = filter,
      transform = (x: I) => {
        /**
           type mismatch;
             [error]  found   : Seq[X]
             [error]  required: X
        */
        otherTransform.apply(transform.apply(x))
      }
    )
  }

  def foreach(newAction : O => Unit) = {
    copy(action = Some(newAction))
  }

  def apply(element : I) = {
    if (filter.map(_.apply(element)).getOrElse(true)) {
      val postTransform = transform.apply(element)
      action.foreach(_.apply(postTransform))
    }
  }
}
object LazyChain {
  def apply[X]() : LazyChain[X, X] = {
    new LazyChain(filter = None, transform = x => x, action = None)
  }
}
driangle
  • 11,601
  • 5
  • 47
  • 54
  • 2
    Scala has [Views](https://docs.scala-lang.org/overviews/collections/views.html) which transform any collection in a lazy collection. Maybe there are some helpful hints there –  Feb 14 '19 at 07:39
  • 1
    `akka-streams` will allow you to do this. But it might be too "heavy" for you – Ivan Stanislavciuc Feb 14 '19 at 11:02
  • 2
    Iteratees let you do stuff like this pretty naturally: you can build up transformations (_enumeratees_) and plug them into sources (_enumerators_) and sinks (_iteratees_), and these are all just values. – Travis Brown Feb 14 '19 at 14:59
  • (Full disclosure: I maintain [an iteratee library for Scala](https://meta.plasm.us/posts/2016/01/08/yet-another-iteratee-library/).) – Travis Brown Feb 14 '19 at 14:59
  • Thanks for those suggestions guys. I will take a look at all three. I'm mainly interested in a lightweight solution that I could implement myself (also for my own knowledge). I'm sure I will learn by looking at the implementation of those libraries. – driangle Feb 14 '19 at 15:13
  • A faster alternative to akka-streams is [Monix](https://monix.io/). – Markus Appel Feb 14 '19 at 16:28

1 Answers1

3

All you want is to wrap a function I => List[O] with some fancy methods. You could write your implicit class to add these methods to this type, but Kleisli does most of this for free, via various cats type classes, mainly FilterFunctor.

  import cats.implicits._
  import cats.data.Kleisli

  type LazyChain[I, O] = Kleisli[List, I, O]
  def lazyChain[A]: LazyChain[A, A] = Kleisli[List, A, A](a => List(a))

  val chainA = lazyChain[Int]
    .filter(_ > 1)
    .map(x => x * 2)
    .map(println)

  val chainB = lazyChain[Int]
    .filter(_ > 5)
    .flatMapF(x => List(x, x))
    .map(println)

  chainA(2)  // should print "4"
  chainA(1)  // should print nothing
  chainB(10) // should print "10" - twice

It might look a bit too magical, so here is a handmade version:

case class LazyChain[A, B](run: A => List[B]) {

  def filter(f: B => Boolean): LazyChain[A, B] = chain(_.filter(f))

  def map[C](f: B => C): LazyChain[A, C] = chain(_.map(f))

  def flatMap[C](f: B => List[C]): LazyChain[A, C] = chain(_.flatMap(f))

  def chain[C](f: List[B] => List[C]): LazyChain[A, C] = LazyChain(run andThen f)
}
object LazyChain {
  def apply[I]: LazyChain[I, I] = new LazyChain(a => List(a))
}

Chaining transformations is a common concern and, as the comments say, using something like monix.Observable, iteratees, etc is the proper way to approach this problem (rather than a plain List and streams are naturally lazy.

V-Lamp
  • 1,630
  • 10
  • 18