Scalaz iteratees: "Lifting" `EnumeratorT` to match `IterateeT` for a "bigger" monad

lmm picture lmm · Nov 13, 2014 · Viewed 14.1k times · Source

If I have an EnumeratorT and a corresponding IterateeT I can run them together:

val en: EnumeratorT[String, Task] = EnumeratorT.enumList(List("a", "b", "c"))
val it: IterateeT[String, Task, Int] = IterateeT.length

(it &= en).run : Task[Int]

If the enumerator monad is "bigger" than the iteratee monad, I can use up or, more generally, Hoist to "lift" the iteratee to match:

val en: EnumeratorT[String, Task] = ...
val it: IterateeT[String, Id, Int] = ...

val liftedIt = IterateeT.IterateeTMonadTrans[String].hoist(
  implicitly[Task |>=| Id]).apply(it)
(liftedIt &= en).run: Task[Int]

But what do I do when the iteratee monad is "bigger" than the enumerator monad?

val en: EnumeratorT[String, Id] = ...
val it: IterateeT[String, Task, Int] = ...

it &= ???

There doesn't seem to be a Hoist instance for EnumeratorT, nor any obvious "lift" method.

Answer

Travis Brown picture Travis Brown · Sep 28, 2019

In the usual encoding an enumerator is essentially a StepT[E, F, ?] ~> F[StepT[E, F, ?]]. If you try to write a generic method converting this type into a Step[E, G, ?] ~> G[Step[E, G, ?]] given an F ~> G, you'll quickly run into an issue: you need to "lower" a Step[E, G, A] to a Step[E, F, A] in order to be able to apply the original enumerator.

Scalaz also provides an alternative enumerator encoding that looks like this:

trait EnumeratorP[E, F[_]] {
  def apply[G[_]: Monad](f: F ~> G): EnumeratorT[E, G]
}

This approach allows us to define an enumerator that's specific about the effects it needs, but that can be "lifted" to work with consumers that require richer contexts. We can modify your example to use EnumeratorP (and the newer natural transformation approach rather than the old monad partial order):

import scalaz._, Scalaz._, iteratee._, concurrent.Task

def enum: EnumeratorP[String, Id] = ???
def iter: IterateeT[String, Task, Int] = ???

val toTask = new (Id ~> Task) { def apply[A](a: A): Task[A] = Task(a) }

We can now compose the two like this:

scala> def result = (iter &= enum(toTask)).run
result: scalaz.concurrent.Task[Int]

EnumeratorP is monadic (if the F is applicative), and the EnumeratorP companion object provides some functions to help with defining enumerators that look a lot like the ones on EnumeratorT—there's empty, perform, enumPStream, etc. I guess there have to be EnumeratorT instances that couldn't be implemented using the EnumeratorP encoding, but off the top of my head I'm not sure what they would look like.