Last active
February 27, 2019 17:32
-
-
Save rkuhn/2870fcee4937dda2cad5 to your computer and use it in GitHub Desktop.
Trying out workarounds for SI-2712: we are running into it when trying to generically extend the FlowOps / FlowOpsMat types in Akka Streams’ Scala DSL.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import scala.annotation.unchecked.uncheckedVariance | |
object Trial2712 { | |
trait NotUsed | |
import language.higherKinds | |
/* | |
* The following are simplified versions of FlowOps / FlowOpsMat and their | |
* concrete subtypes. | |
*/ | |
trait FO[+Out, +Mat] { | |
type Repr[+O] <: FO[O, Mat] { | |
type Repr[+OO] = FO.this.Repr[OO] | |
} | |
def map[T](f: Out => T): Repr[T] = ??? | |
} | |
trait FOM[+Out, +Mat] extends FO[Out, Mat] { | |
type Repr[+O] <: FOM[O, Mat] { | |
type Repr[+OO] = FOM.this.Repr[OO] | |
type ReprMat[+OO, +MM] = FOM.this.ReprMat[OO, MM] | |
} | |
type ReprMat[+O, +M] <: FOM[O, M] { | |
type Repr[+OO] = FOM.this.ReprMat[OO, M @uncheckedVariance] | |
type ReprMat[+OO, +MM] = FOM.this.ReprMat[OO, MM] | |
} | |
def mapMat[T](f: Mat => T): ReprMat[Out, T] = ??? | |
} | |
class Source[+O, +M] extends FOM[O, M] { | |
type Repr[+OO] = Source[OO, M @uncheckedVariance] | |
type ReprMat[+OO, +MM] = Source[OO, MM] | |
} | |
class Flow[-I, +O, +M] extends FOM[O, M] { | |
type Repr[+OO] = Flow[I @uncheckedVariance, OO, M @uncheckedVariance] | |
type ReprMat[+OO, +MM] = Flow[I @uncheckedVariance, OO, MM] | |
} | |
class SubSource[+O, +M] extends FO[O, M] { | |
type Repr[+OO] = SubSource[OO, M @uncheckedVariance] | |
} | |
class SubFlow[-I, +O, +M] extends FO[O, M] { | |
type Repr[+OO] = SubFlow[I @uncheckedVariance, OO, M @uncheckedVariance] | |
} | |
/* | |
* These are helpers to work around SI-2712 which could be provided by Akka Streams. | |
*/ | |
trait U[X] { | |
type Out | |
type Mat | |
type R[+o] <: FO[o, Mat] { | |
type Repr[+O] = R[O] | |
} | |
def ch(x: X): R[Out] | |
} | |
implicit def uOne[O, M, TC[+o, +m] <: FO[o, m] { type Repr[+O] = TC[O, m @uncheckedVariance] }] = | |
new U[TC[O, M]] { | |
type Out = O | |
type Mat = M | |
type R[+O] = TC[O, Mat] | |
def ch(x: TC[O, M]) = x | |
} | |
implicit def uTwo[I, O, M, TC[i, +o, +m] <: FO[o, m] { type Repr[+O] = TC[i, O, m @uncheckedVariance] }] = | |
new U[TC[I, O, M]] { | |
type Out = O | |
type Mat = M | |
type R[+O] = TC[I, O, Mat] | |
def ch(x: TC[I, O, M]) = x | |
} | |
trait Umat[X] { | |
type Out | |
type Mat | |
type R[+o, +m] <: FOM[o, m] { | |
type Repr[+O] = R[O, m @uncheckedVariance] | |
type ReprMat[+O, +M] = R[O, M] | |
} | |
def ch(x: X): R[Out, Mat] | |
} | |
implicit def uOneMat[O, M, TC[+o, +m] <: FOM[o, m] { type Repr[+O] = TC[O, m @uncheckedVariance]; type ReprMat[+O, +M] = TC[O, M] }] = | |
new Umat[TC[O, M]] { | |
type Out = O | |
type Mat = M | |
type R[+o, +m] = TC[o, m] | |
def ch(x: TC[O, M]) = x | |
} | |
implicit def uTwoMat[I, O, M, TC[i, +o, +m] <: FOM[o, m] { type Repr[+O] = TC[i, O, m @uncheckedVariance]; type ReprMat[+O, +M] = TC[i, O, M] }] = | |
new Umat[TC[I, O, M]] { | |
type Out = O | |
type Mat = M | |
type R[+o, +m] = TC[I, o, m] | |
def ch(x: TC[I, O, M]) = x | |
} | |
/* | |
* This is how client code can now use the helpers to write extension methods that | |
* work across all FlowOps subtypes. | |
*/ | |
implicit class x[X](val x: X) extends AnyVal { | |
def xx(i: Int)(implicit u: U[X]) = u.ch(x).map(identity).map(identity) | |
def xy(implicit u: U[X]) = u.ch(x).map(_.toString).map(identity) | |
def m[T1, T2](f: T1 => T2)(implicit u: Umat[X] { type Mat <: T1 }) = | |
u.ch(x).mapMat(f) | |
} | |
/* | |
* These don’t work as intended: it seems impossible to get the types | |
* out there. This results in that an expression like | |
* | |
* f.mm(_ => "buh") | |
* | |
* might compile, but its returned type will not longer unify with | |
* Flow et al. It seems that the path-dependent type leaks out in the | |
* form of an existential. | |
*/ | |
class Y[X](val x: X)(implicit val u: Umat[X]) { | |
def mm[T](f: u.Mat => T) = u.ch(x).mapMat(f) | |
def xz[T](f: u.Out => T) = u.ch(x).map(f) | |
} | |
implicit def y[X](x: X)(implicit u: Umat[X]) = new Y(x) | |
/* | |
* All these compile successfully. Type ascriptions to lambda arguments are unfortunately | |
* required, the attempt presented in class Y above does not work. | |
*/ | |
val s1 = new Source[Int, NotUsed].xx(12).m((_: NotUsed) => "").map(_ + 12) | |
val s2: Source[Int, String] = s1 | |
val f1 = new Flow[Int, Int, NotUsed].xx(12) | |
val f2: Flow[Int, Int, NotUsed] = f1 | |
val f3 = f1.m((_: NotUsed) => 42).xy | |
val f4: Flow[Int, String, Int] = f3 | |
val subs1 = new SubSource[Int, NotUsed].xy | |
val subs2: SubSource[String, NotUsed] = subs1 | |
val subf1 = new SubFlow[Int, Int, NotUsed].xy | |
val subf2: SubFlow[Int, String, NotUsed] = subf1 | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
You're encountering the exact issues that Miles has been trying to solve (and others & me too)...
in
UMat
, you do not manipulate aR[_]
but aR[_, _]
and as soon as you try to fix one of the type params to obtain aR[_]
, scalac isn't able to unify types and fails...The solution of Miles is to force scalac to "reify" types
R[_]
using Unapply (as you've done) plusAux
technique (to clearly reify types) plus a macro to brute-force scalac to become aware of type unification.Have a look at
Generic1
in shapeless, this is where those tricks come from...That's what I've done in a simpler way in Precepte too https://github.com/MfgLabs/precepte/blob/master/precepte-core-cats/src/main/scala/HackSI2712.scala#L39 ... I know what I'm looking for so the macro is much simpler...
This could work in your case too as you know you're manipulating
Source/Flow/Sink
...But if scalac could do that in a generic way, it would help so many of us that now use higher-kinds a lot in their code :D