Open raulraja opened 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:
case l@BobOp
line, in which it gives a type mismatch
error, in which it requires MM1[AA1]
, but it finds MM1[A]
instead.case l@KarlOp
line, but in this case the message is that, in the argument of the call to karl
, it expects an A
but instead it gets a l.a.type
. In other words, it does not recognise that the field l.a
is also of type A
.Fiddling with the example program above, I have found so far two directions, that could allow us to put the type parameters back:
A
to every method in the trait
, and every delegate method in the handler, and every method in the To
.object Ann
into a class AnnImpl[A]
, since the class can have type parameters.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
}
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]
}
@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
.
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.
Update: in a recent PR and issue, we now support type parameters in the @tagless
annotation without the stacksafe
.
Currently no type args are supported in @free and @tagless algebras forcing to define Algebras nested in dependent types such as in the
reader
orstate
effects in theeffects
module. We should remove this limitation if possible to allow any number of arbitrary type args beside the implicitF[_]
that they all have.