frees-io / freestyle

A cohesive & pragmatic framework of FP centric Scala libraries
http://frees.io/
Apache License 2.0
614 stars 50 forks source link

Support additional type args in @free and @tagless algebras #247

Open raulraja opened 7 years ago

raulraja commented 7 years ago

Currently no type args are supported in @free and @tagless algebras forcing to define Algebras nested in dependent types such as in the reader or state effects in the effects module. We should remove this limitation if possible to allow any number of arbitrary type args beside the implicit F[_] that they all have.

diesalbla commented 7 years ago

Here is a simple example in which I am seeing the problems:

@free trait Ann[A]{
  def bob(i: Int): FS[A]
  def karl(a: A): FS[Int]
}

The macro expands this @free to the following Scala code:

trait Ann[FF1[_], A] extends freestyle.EffectLike[FF1] {
  def bob(i: Int): FS[A]
  def karl(a: A): FS[Int]
}

object Ann {

  sealed abstract trait Op[AA1] 
  case class BobOP[A](i: Int) extends Op[A] 
  case class KarlOP[A](a: A) extends Op[Int]

  abstract trait Handler[MM1[_], A] extends FunctionK[Op, MM1] {
    protected[this] def bob(i: Int): MM1[A]
    protected[this] def karl(a: A): MM1[Int]
    override def apply[AA1](fa1: Op[AA1]): MM1[AA1] = fa1 match {
      case (l @ BobOP(_)) => bob(l.i)
      case (l @ KarlOP(_)) => karl(l.a)
    }
  }

  class To[LL1[_], A](ii1: Inject[Op, LL1]) extends Ann[LL1, A] {
    private[this] val toInj1 = FreeS.inject[Op, LL1](ii1)
    override def bob(i: Int): FS[A] = toInj1(BobOP(i))
    override def karl(a: A): FS[Int] = toInj1(KarlOP(a))
  }

  implicit def to[LL1[_], A](implicit ii1: Inject[Op, LL1]): To[LL1, A] = new To[LL1, A]()
  def apply[LL1[_], A](implicit ev1: Ann[LL1, A]): Ann[LL1, A] = ev1
}

The generated Scala code gives the following two errors:

diesalbla commented 7 years ago

Fiddling with the example program above, I have found so far two directions, that could allow us to put the type parameters back:

diesalbla commented 7 years ago

With the ~second~ first option mentioned, i.e. to add the type parameters from the @free trait to the elements in the companion object:

trait Ann[FF1[_]] extends freestyle.EffectLike[FF1] {
  def bob[A](i: Int): FS[A]
  def karl[A](a: A): FS[Int]
}

object Ann {

  sealed abstract trait Op[AA1] 
  case class BobOP[A](i: Int) extends Op[A]
  case class KarlOP[A](a: A) extends Op[Int]

  trait Handler[MM1[_]] extends FunctionK[Op, MM1] {
    protected[this] def bob[A](i: Int): MM1[A]
    protected[this] def karl[A](a: A): MM1[Int]

    override def apply[AA1](fa1: Op[AA1]): MM1[AA1] = fa1 match {
      case (l @ BobOP(_) ) => bob(l.i)
      case (l @ KarlOP(_)) => karl(l.a)
    }
  }

  class To[LL1[_]](implicit ii1: Inject[Op, LL1]) extends Ann[LL1] {
    private[this] val toInj1 = FreeS.inject[Op, LL1](ii1)
    override def bob[A](i: Int): FS[A] = toInj1(BobOP(i))
    override def karl[A](a: A): FS[Int] = toInj1(KarlOP(a))
  }

  implicit def to[LL1[_]](implicit ii1: Inject[Op, LL1]): To[LL1] = new To[LL1]()
  def apply[LL1[_], A](implicit ev1: Ann[LL1]): Ann[LL1] = ev1
}
diesalbla commented 7 years ago

The ~second~ first alternative, to add the type parameter to the common container (thus now has to be a class), it would then become:

trait Ann[FF1[_], A] extends freestyle.EffectLike[FF1] {
  def bob(i: Int): FS[A]
  def karl(a: A): FS[Int]
}

class AnnProvider[A] {

  sealed abstract trait Op[AA1] 
  case class BobOP(i: Int) extends Op[A]
  case class KarlOP(a: A) extends Op[Int]

  trait Handler[MM1[_]] extends FunctionK[Op, MM1] {
    protected[this] def bob(i: Int): MM1[A]
    protected[this] def karl(a: A): MM1[Int]

    override def apply[AA1](fa1: Op[AA1]): MM1[AA1] = fa1 match {
      case (l @ BobOP(_) ) => bob(l.i)
      case (l @ KarlOP(_)) => karl(l.a)
    }
  }

  class To[LL1[_]](implicit ii1: Inject[Op, LL1]) extends Ann[LL1, A] {
    private[this] val toInj1 = FreeS.inject[Op, LL1](ii1)
    override def bob(i: Int): FS[A] = toInj1(BobOP(i))
    override def karl(a: A): FS[Int] = toInj1(KarlOP(a))
  }

  implicit def to[LL1[_]](implicit ii1: Inject[Op, LL1]): To[LL1] = new To[LL1]()

  def apply[LL1[_]](implicit ev1: Ann[LL1, A]): Ann[LL1, A] = ev1
}

object Ann {
  def apply[A] = new AnnProvider[A]
}
diesalbla commented 7 years ago

@raulraja ¿Have you tried before any of these alternatives? ¿Do you have any preference?

The first option may have the problem that, since each method now becomes parametrised, it may be necessary to add type parameters to every method call. Another possible problem is that the Handler and the To classes have each a separate type parameter, so they may not be aligned.

The second option may have the problem of introducing this new class. Also, the syntax for using parametrised type-classes would not be too different from the current one. As in the current effects, we would still need to write something like

val p = Ann[Double]
def program: p[p.Op]

Which is to say, we would still need to use a middle declaration for accessing the To or Handler.

raulraja commented 7 years ago

I think both approaches affect so much to the final syntax and affect the user experience that we should settle this for now until we have a better way to deal with this problem. The current workaround is to define a wrapper class as in the state and reader effects and that is a much more sane approach for users than having to pass type args on each call or referring to explicit instances for To or Handler. I'll leave this issue as open untagged to revisit down the road. We have bigger issues right now such as the scala meta support to get proper syntax highlighting working on IntelliJ.

diesalbla commented 6 years ago

Update: in a recent PR and issue, we now support type parameters in the @tagless annotation without the stacksafe.