scalafunctional-programmingscala-catsfree-monad

Using the Free Monad in Functional Domain Design


I'm quite new to functional programming. However, I read about the Free Monad, and I'm trying to use it in a toy project. In this project, I model the stock's portfolio domain. As suggested in many books, I defined an algebra for the PortfolioService and an algebra for the PortfolioRepository.

I want to use the Free monad in the definition of the PortfolioRepository algebra and interpreter. For now, I did not define the PortfolioService algebra in terms of the Free monad.

However, if I do so, in the PortfolioService interpreter, I cannot use the algebra of the PortfolioRepository because of different used monads. For example, I cannot use the monads Either[List[String], Portfolio], and Free[PortfolioRepoF, Portfolio] inside the same for-comprehension :(

I doubt that if I start to use the Free monad to model an algebra, all the other algebra that need to compose with it must be defined in terms of the Free monad.

Is it true?

I am using Scala and Cats 2.2.0.


Solution

  • 99% of the time Free monad is interchangeable with Tagless final:

    The only difference is when do you interpret:

    Nowadays things moved in favor of tagless final. Free monad is used internally in IO monad implementation (Cats Effect IO, Monix Task, ZIO) and in e.g. Doobie (though from what I heard Doobie's author was thinking about rewriting it into tagless, or at least regretting not using tagless?).

    If you want to learn how to use that in modelling there is a book by Gabriel Volpe - Practical FP in Scala that uses tagless final as well as my own small project that uses Cats, FS2, Tapir, tagless etc which can demonstrate some ideas.

    If you intend to use Free, then well, there are some challenges:

    sealed trait DomainA[A] extends Product with Serializable
    object DomainA {
      case class Service1(input1: X, input2: Y) extends DomainA[Z]
      // ...
    
      def service1(input1: X, input2: Y): Free[DomainA, Z] =
        Free.liftF(Service1(input1, input2))
    }
    
    val interpreterA: DomainA ~> IO = ...
    

    You use Free[DomainA, *], combine it using .map, .flatMap, etc, interpret it with interpretA.

    Then you add another domain, DomainB. And the fun begins:

    In tagless final there is always a dependency but almost always on one type - F - and empirical evidences of many people shows that is easier to use despite being theoretically equal in power to a free monad. But it is not a scientific argument, so feel free to experiment with free monad on your own. See e.g. this Underscore article about using multiple DSLs at once.

    Whether you pick one or the other you are NOT forced to use it everywhere - everything that is Free can be (should be) interpreted into a specific implementation, tagless makes you pass the specific implementation as argument so you can use either for a single component, that is interpreted on its edge.