Search code examples
scalascalazfree-monad

What's the best way to do streaming in your free algebra?


I've been experimenting with creating an HTTP client using Free monads, similar to the approach taken in the talk given by Rúnar Bjarnason, Composable application architecture with reasonably priced monads.

What I have so far is can be seen in this snippet, https://bitbucket.org/snippets/atlassian-marketplace/EEk4X.

It works ok, but I'm not entirely satisfied. The biggest pain point is caused by the need to parameterize HttpOps over the algebra it will be embedded in in order to allow streaming of the request and response bodies. This makes it impossible build your algebra by simply saying

type App[A] = Coproduct[InteractOps, HttpcOps[App, ?], A]

If you try, you get an illegal cyclic reference error from the compiler. To solve this, you can use a case class

type App0[A] = Coproduct[InteractOps, HttpcOps[App, ?], A]   
case class App[A](app0: App0[A])

This solves the cyclic reference problem, but introduces a new issue. We no longer have Inject[InteractOps, App] instances readily available, which means we no longer have the Interact[App] and Httpc[HttpcOps[App, ?]] instances, so we have to manually define them for our algebra. For a small and simple algebra like this one, that isn't too onerous, but for something bigger it can turn into a lot of boilerplate.

Is there another approach that would allow us to include streaming and compose algebras in a more convenient manner?


Solution

  • I'm not sure I understand why App needs to refer to itself. What is e.g. the expected meaning of this:

    App(inj(Send(
      Request(GET, uri, v, hs,
              StreamT(App(inj(Send(
                Request(GET, uri, v, hs,
                        StreamT(App(inj(Tell("foo")))))))))))))
    

    That is, an EntityBody can for some reason be generated by a stream of HTTP requests nested arbitrarily deep. This seems like strictly more power than you need.

    Here's a simple example of using a stream in a free monad:

    case class Ask[A](k: String => A)
    case class Req[F[_],A](k: Process[Ask, String] => A)
    type AppF[A] = Coproduct[Ask, Req[Ask,?], A]
    type App[A] = Free[AppF, A]
    

    Here, each Req gives you a stream (scalaz.stream.Process) of Strings. The strings are generated by asking something for the next string (e.g. by reading from standard input or an HTTP server or whatever). But note that the suspension functor of the stream isn't App, since we don't want to give a Req the opportunity to generate other Reqs.