3
votes

So I've been getting deeper into FP concepts and I liked the concept of purity enclosed in the IO monad. Then I read this, and thought that the IO monad is indeed not as decoupled(?) as using Free Monads.

So I started doing my stuff using those concepts and then I realized that traits achieve the same purpose of separating structure from execution. Even worse than that, using free monads has a lot of limitations, like error handling and passing context bounds and implicit parameters into the interpreter/implementation.

So my question is: what's the advantage of using them? how do I solve the problems I just mentioned (implicit params & error handling)? Does the use of Free Monands limit to the academic realm, or can it be used in the industry?

Edit: An example to explain my doubts

  import cats.free.Free._
  import cats.free.Free
  import cats.{Id, ~>}

  import scala.concurrent.Future

  sealed trait AppOpF[+A]

  case class Put[T](key: String, value: T) extends AppOpF[Unit]

  case class Delete(key: String) extends AppOpF[Unit]
  //I purposely had this extend AppOpF[T] and not AppOpF[Option[T]]
  case class Get[T](key: String) extends AppOpF[T]

  object AppOpF {
    type AppOp[T] = Free[AppOpF, T]

    def put[T](key: String, value: T): AppOp[Unit] = liftF[AppOpF, Unit](Put(key, value))

    def delete(key: String): AppOp[Unit] = liftF[AppOpF, Unit](Delete(key))

    def get[T](key: String): AppOp[T] = liftF[AppOpF, T](Get(key))

    def update[T](key: String, func: T => T): Free[AppOpF, Unit] = for {
      //How do I manage the error here, if there's nothing saved in that key?
      t <- get[T](key)
      _ <- put[T](key, func(t))
    } yield ()


  }

  object AppOpInterpreter1 extends (AppOpF ~> Id) {
    override def apply[A](fa: AppOpF[A]) = {
      fa match {
        case Put(key,value)=>
          ???
        case Delete(key)=>
          ???
        case Get(key) =>
          ???
      }
    }
  }
  //Another implementation, with a different context monad, ok, that's good
  object AppOpInterpreter2 extends (AppOpF ~> Future) {
    override def apply[A](fa: AppOpF[A]) = {
      fa match {
        case a@Put(key,value)=>
          //What if I need a Json Writes or a ClassTag here??
          ???
        case a@Delete(key)=>
          ???
        case a@Get(key) =>
          ???
      }
    }
  }
1
It would be easier if you provided an example of such replacement. Of course it free monads is not the only way to deal with this problem. But I guess you are about to reinvent the final-tagless approach. - simpadjo
Free monad / free applicative lets you build a program as a pure data structure, which can be treated as a value, and can be easily composed, reused, passed around, and interpreted in different ways, just like any other ordinary value. I'm not sure how you can achieve this with plain old traits - you may want to update your example to show how your plain old traits work. - Ziyang Liu
You're right, I was talking about IOmonads&traits conjunction - Alejandro Navas

1 Answers

0
votes

Free algebra with IO monad serves the same purpose - to build a program as pure data structures. If you compare Free with some concrete implementation of IO, IO would probably win. It'll have more features and specialized traits that'll help you move fast and develop your program quickly. But it'll also mean that you'll have a major vendor lock on one implementation of IO. Whichever IO you choose, it'll be a concrete IO library that may have performance issues, bugs or maybe support problems - who knows. And changing your program from one vendor to another will cost you a lot because of this tight coupling between your program and implementation.

Free algebra, on the other hand, allows you to express your program without talking about your program's implementation. It separates your requirements from implementation in a way that you can test both easily and change them independently. As another benefit, Free allows you not to use IO at all. You can wrap standard Futures, java's standard CompletableFuture or any other third-party concurrency primitives in it and your program will still be pure. And for that, Free will require additional boilerplate (just as you showed in your example) and less flexibility. So the chose is yours.

There's also another way - final tagless. It's the approach that tries to balance pros from both sides providing less vendor lock and still not be as verbose as Free algebra. Worth checking it out.