ProjectSeptemberInc / freek

Freek, a freaky simple Free to combine your DSL seamlessly
Other
198 stars 19 forks source link

DSL composition and Modularity #4

Open debasishg opened 8 years ago

debasishg commented 8 years ago

Consider the following example that uses free monads:

import scalaz.Free

sealed trait Account

sealed trait RepoF[A]

// the algebraic data types  
case class Query(no: String) extends RepoF[Account]
case class Store(account: Account) extends RepoF[Unit]
case class Delete(no: String) extends RepoF[Unit]

// the free monad
type Repo[A] = Free[RepoF, A]

trait Repository {
  def store(account: Account): Repo[Unit] = 
    Free.liftF(Store(account))

  def query(no: String): Repo[Account] =
    Free.liftF(Query(no))

  def delete(no: String): Repo[Unit] = 
    Free.liftF(Delete(no))

  def update(no: String, f: Account => Account): Repo[Unit] = for {
    a <- query(no)
    _ <- store(f(a))
  } yield ()
}

Note how I can write the update function as part of the module Repository by composing the other ADTs. I can do this as AccountRepo is a free monad. Now let's have a look at the freek version ..

import cats.data.Xor
import freek._

sealed trait Account

trait Repository {
  sealed trait Repo[A]

  case class Query(no: String) extends Repo[Xor[String, Account]]
  case class Store(account: Account) extends Repo[Xor[String, Account]]
  case class Delete(no: String) extends Repo[Xor[String, Unit]]

  def query(no: String) = Query(no)
  def store(account: Account) = Store(account)
  def delete(no: String) = Delete(no)

  // How do I write this function here ?
  def update(no: String, f: Account => Account) = ???
}

The question is how do I define update here ? Note I am still within the module Repository and have not yet defined my final co-product like PRG or O as in the examples of freek. With freek, we deal with pure ADTs within the module which is not a monad - hence I need to wait till the final composition of the DSL (which will involve other modules as well) to create such functions.

Is there any way in freek to have free monads like the above in the individual modules and then use the onionT trick just for the monad transformation (using hk co-products) and interpreter composition ?

mandubian commented 8 years ago

@debasishg check that if you missed my tweet

https://github.com/ProjectSeptemberInc/freek/blob/master/src/test/scala/AppSpec.scala#L668-L731

debasishg commented 8 years ago

@mandubian - Thanks for the fix ..

One thing I noticed is if I remove the object Bar here (https://github.com/ProjectSeptemberInc/freek/blob/master/src/test/scala/AppSpec.scala#L708) (it's not being used), and change type PRG = Foo :|: Log.DSL :|: Bar.PRG :||: Repo.PRG to type PRG = Foo :|: Log.DSL :|: Bar :||: Repo.PRG in object PRG at https://github.com/ProjectSeptemberInc/freek/blob/master/src/test/scala/AppSpec.scala#L714, then the unification fails in https://github.com/ProjectSeptemberInc/freek/blob/master/src/test/scala/AppSpec.scala#L721. Possibly related to the types that :||: takes ..

debasishg commented 8 years ago

One more thought - if managing both :|: and :||: becomes difficult, another option may be to allow the user write the free monad Free[A] in the local program (like the first example that I gave).

And in the larger program have a combinator that lifts each individual Free[_] into the higher kinded coproduct like PRG. This way we don't abstract the Free but allow nicer modularity - local programs compose locally, in the global context lift into the coproduct and execute using interpreter composition.

Just a thought though ..

mandubian commented 8 years ago

@debasishg actually, I'd like, if possible, to avoid writing Free in local programs if not required because DSL are often simple, auto-sufficient & don't need to be combined like the update case. But if you need it, you can do it too... I'll re-think about it...

Yet, in our case, the solution is quite simple actually:

type PRG = Foo :|: Log.DSL :|: Bar :||: Repo.PRG simply doesn't compile because :||: require 1 non-fxnil FX on the left and 1 FX on the right ... In this case, if you use:

type PRG = Foo :|: Log.DSL :|: Bar :|: Repo.PRG, it works...

Let do the analogy with List:

In previous versions of freek, I had tried to get rid of :||: operator but we need to be able to merge 2 FX as your sample has shown.

Does it seem understandable or is it too fuzzy?

debasishg commented 8 years ago

because :||: require 1 non-fxnil FX on the left and 1 FX on the right

Yes, I got that part. Was just thinking that we may have to introduce additional types like the unused object Bar in the example above. Otherwise it looks good ..

mandubian commented 8 years ago

Actually, the Bar object was just to have 2 programs to combine because this is the case we had and to be able to write Bar.PRG which is quite explicit.

My reluctance against cake pattern make me tend to put query/store/delete/update helpers in an object Repo instead of a layer... It reduces the burden on scalac but it reduces a bit the modularity aspect... compromise :D

BTW, we should test the case when we have a local program that itself combines sub-programs to see how Scalac behaves...

mandubian commented 8 years ago

@debasishg if you check FX code & specially :||: you'll see that it's a pure type trick... :||: doesn't even provide Head/Tail sub-types of FXCons... :||: is a pure synthetic type just used as a ground for implicit inferences... quite interesting actually ;)

debasishg commented 8 years ago

Actually, the Bar object was just to have 2 programs to combine because this is the case we had and to be able to write Bar.PRG which is quite explicit.

👍

I plan to take a deeper look at the implementation to understand the type wizardry .. nice stuff in there :-)

mandubian commented 8 years ago

Yes don't hesitate ;) Need to improve code IMHO & limit as much as possible the number of implicit resolutions to limit the impact on compile time but I feel like it becomes really usable as is and doesn't make anything weird: it's just helpers to build types combining types but at the end, it's just a Free... The other work field is the type errors: i try to make the code generate errors that are understandable... But, it would be so cool to be able to customize errors much more in scalac...