Compositional batching using functional constructs

Viewed 51

I am trying to create a compositional batching mechanism.

The process flow must be.

  1. Accumulate the state in the form of s: Map[BatchFragmentId, FragmentRequest]
  2. Compute the result in the form of Map[BatchFragmentId, FragmentResponse]
  3. Perform a series of pre-defined computations which are of the form f: Map[BatchFragmentId, FragmentResponse] => R for some result R.

My solution

Let Batch[R] be a monad, which contains some acccumualted state s and some function f.

map[B](g: R => B) makes perfect sense since it is simply a compositon of the self-contained function f (Batch.make(f=f.andThen(g), s=s).

flatMap[B](f: R => Batch[B]) on the hand does not make much sense, since R requires the computation to be completed, thus Batch[B] cannot exist at this point, since s must contain Batch[B]'s state when performing the response computation.

Modifying flatMap:

def combine[B, C](that: Batch[B])(t: (A, B) => C): Batch[C] = {
  val combined: Map[BatchFragmentId, FragmentRequest] = that.state ++ state
  val g: Map[BatchFragmentId, FragmentResponse] => C = 
    (r: Map[BatchFragmentId, FragmentResponse]) => t(f(r), that.f(r))
  Batch.make(combined, g)
}

This does not play well with Scala's for comprehensions and it's readability deteriorates more rapidly, than a for comprehension.

val b: Batch[String] = ???
val b2: Batch[String] = ???
val b3: Batch[Int] = ???
val combined: Batch[(String, String)] = 
  (b.combine(b2){ case (s1, s2) => s1 + s2 })
  .combine(b3){ case (s1, i1) => s1 + i1.toString }

Which can become very messy given a couple of batches. The desired way to be able compose these batches would be.

val o: Batch[(String, String)] = for {
  _ <- put("key", "42")
  x1 <- get("key")
  _ <- put("key2", "24")
  x2 <- get("key2")
} yield (x1, x2)

But any elegant syntax would be a solution.

I am not that well versed in category theory and have limited experience with writing code in this form, so I am unsure of what type classes to look into for this particular problem.

Is this even the right approach for such a problem; am I modelling the problem appropriately? Is this problem already well understood and generalized?

I have cats at my disposal also.

1 Answers

After digging through the various typeclasses provided by cats, I found Applicative to be very suitable for this problem.

For anyone interested in the result, I ended up with the following.

type BatchFragmentId = String
trait FragmentRequest
trait FragmentResponse
type S = Map[BatchFragmentId, FragmentRequest]

object Batch {
  def make[A](s: S, f: Map[BatchFragmentId, FragmentResponse] => A): Batch[A] = Batch[A](
   transformer = f,
   state = s
  )
}

implicit object BatchApplicative extends Applicative[Batch] {
  override def pure[A](x: A): Batch[A] = Batch[A](
    transformer = _ => x,
    state =  Map.empty
  )

  override def ap[A, B](ff: Batch[A => B])(fa: Batch[A]): Batch[B] = {
    val f: Map[String, FragmentResponse] => B = (r: Map[BatchFragmentId, FragmentResponse]) => ff.transformer(r)(fa.transformer(r))
    Batch[B](
      transformer = f,
      state =  ff.state ++ fa.state
    )
  }
}

case class Batch[A] (
                      transformer: Map[BatchFragmentId, FragmentResponse] => A,
                      state: S
                    )

implicit class BatchOps[A](fa: Batch[A])(implicit A: Applicative[Batch]) {
  def <>[B](fb: Batch[B]): Batch[(A, B)] = A.product(fa, fb)
}

// Example
case class SomeFragmentRequest(x: Int) extends FragmentRequest
case class SomeFragmentResponse(x: Int) extends FragmentResponse

val id: BatchFragmentId = "some-id"
val exampleBatch = Batch.make[SomeFragmentResponse](
  s = Map(id -> SomeFragmentRequest(42)),
  f = responses => responses.get(id).map{ case x: SomeFragmentResponse => x }.get
)

// Example of composition
val o: Batch[(SomeFragmentResponse, SomeFragmentResponse)] = exampleBatch <> exampleBatch
// N arity
val o2: Batch[(SomeFragmentResponse, SomeFragmentResponse, SomeFragmentResponse, SomeFragmentResponse)] =
  Applicative[Batch].tuple4(exampleBatch, exampleBatch, exampleBatch, exampleBatch)

def run(s: S): Map[BatchFragmentId, FragmentResponse] = ???
def resolve[R](r: Map[BatchFragmentId, FragmentResponse], b: Batch[R]): R = b.transformer(r)

val res: (SomeFragmentResponse, SomeFragmentResponse) = resolve(run(o.state), o)
Related