Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
212 views
in Technique[技术] by (71.8m points)

scala - 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.

question from:https://stackoverflow.com/questions/65643691/using-the-free-monad-in-functional-domain-design

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

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

  • you can pass Free[S, *] as your Monad instance
  • you can .foldMap Free[S, A] using S ~> F mapping with Monad[F] into F[A]

The only difference is when do you interpret:

  • tagless interprets immediately, so it require you to pass around type class instances for your F, but since F is a type parameter it gives the impression that it is deferred - because it defers the moment when the type is chosen
  • free monad lets you create the value immediately with no dependencies on type classes, you can store them as vals in objects, there are no dependencies on type classes. The price you pay is intermediate representation that you ultimately want to discard as soon as you will be able to interpret into useful result. On the other hand it is missing tagless' ability to constrain your operation only to certain algebras (e.g. only Functor, only Applicative, etc to better control effects in dependencies).

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:

  • you cannot just combine Free[DomainA, *] with Free[DomainB, *] because they are different types, you need to align them to make that possible!
  • so, you have to combine all algebras into one:
    type BusinessLogic[A] = EitherK[DomainA, DomainB, A]
    implicit val injA: InjectK[DomainA, BusinessLogic] = ...
    implicit val injB: InjectK[DomainB, BusinessLogic] = ...
    
  • your services cannot hardcode one algebra, you have to inject current algebra into a "bigger" one:
    def service1[Total[_]](input1: X, input2: Y)(
       implicit inject: InjectK[DomainA, Total]
    ): Free[Total, Z] =
       Free.liftF(inject.inj(Service1(input1, input2)))
    
  • your interpreter is also more complex now:
    val interpreterTotal: EitherK[DomainA, DomainB, *] ~> IO =
       new (EitherK[DomainA, DomainB, *] ~> IO) {
         def apply[A](fa: EitherK[DomainA, DomainB, A]) =
           fa.run.fold(interpreterA, interpreterB)
       }
    
  • and it gets more complex with each new added algebra (EitherK[DomainA, EitherK[DomainB, ..., *], *]).

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.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...