Skip to content

Instantly share code, notes, and snippets.

@rkuhn
Last active February 27, 2019 17:32
Show Gist options
  • Save rkuhn/2870fcee4937dda2cad5 to your computer and use it in GitHub Desktop.
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.
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
}
@rkuhn
Copy link
Author

rkuhn commented Mar 7, 2016

forgot to link to Daniel’s gist that I used for inspiration

@mandubian
Copy link

Can't compile it in scala 2.11.7

first had to define

type NotUsed = Unit

then scalac complains about: (see comment in code):

  trait FOM[+Out, +Mat] extends FO[Out, Mat] {
    // this part seems weird for scalac (is Repr really needed?)
    type Repr[+O] <: FOM[O, Mat] {
      type Repr[+OO] = FOM.this.ReprMat[OO, Mat @uncheckedVariance]
      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] = ???
  }

and

  trait U[X] {
    type Out
    type Mat
    type R[+o] <: FO[o, Mat] /**is the following {} needed?*/ {
      type Repr[+O] = R[O]
    }
    def ch(x: X): R[Out]
  }

@rkuhn
Copy link
Author

rkuhn commented Mar 7, 2016

Ah, right, I used akka.NotUsed in there. But apart from that Scala 2.11.7 should compile this (it does for me).

Update: I was fooled somehow, fixing it.

@rkuhn
Copy link
Author

rkuhn commented Mar 7, 2016

The refinements are necessary in order to be able to fluently chain DSL operators without losing the resulting types. Without them you’d need to manually flatten Repr#Repr#Repr chains, and with them only reasonable implementations are permitted as a bonus.

@rkuhn
Copy link
Author

rkuhn commented Mar 7, 2016

fixed compilation errors and removed Akka dependency

@mandubian
Copy link

ok I see why you have to do that... wan't even aware it was possible to write that in this way and make it compile ;)

@mandubian
Copy link

You're encountering the exact issues that Miles has been trying to solve (and others & me too)...

in UMat, you do not manipulate a R[_] but a R[_, _] and as soon as you try to fix one of the type params to obtain a R[_], 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) plus Aux 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment