Join GitHub today
GitHub is home to over 31 million developers working together to host and review code, manage projects, and build software together.
Sign upSI-2712 Add support for partial unification of type constructors #5102
Conversation
scala-jenkins
added this to the 2.12.0-M5 milestone
Apr 15, 2016
This comment has been minimized.
This comment has been minimized.
|
+1 I think this would be really great to get in for 2.12 -- given that it's off by default and fixes a longstanding known issue. Thanks for your hard work @milessabin! |
This comment has been minimized.
This comment has been minimized.
djspiewak
commented
Apr 15, 2016
|
There are no words to describe the kind of impact this would have on my daily use of Scala. I know it's very late in the 2.12 cycle, but I would love to see this make it. |
This comment has been minimized.
This comment has been minimized.
|
Is there any way to know what impact enabling the flag has on existing code, e.g. running all the tests with the flag enabled? Or, indeed, on the community build. |
This comment has been minimized.
This comment has been minimized.
djspiewak
commented
Apr 15, 2016
|
@fommil The only problem I've seen it cause thus far was compiling scalaz (found by @runarorama). Specifically, it ran into some problems with type tags. I'm not sure if @milessabin fixed that particular bug or not, but that's literally the only problem I've seen. |
This comment has been minimized.
This comment has been minimized.
|
Here is scalaz result. I can remove some |
djspiewak
reviewed
Apr 15, 2016
| // A = Int | ||
| // | ||
| // A more "natural" unifier might be M[t] = [t][t => t]. There's lots of scope for | ||
| // experimenting with alternatives here. |
This comment has been minimized.
This comment has been minimized.
djspiewak
Apr 15, 2016
•
If this does make it in for 2.12 (or even if it doesn't), the above should be expanded into a blog post somewhere. We need to be clear that this is very literally adding left-to-right curried type constructor semantics to Scala. I tend to think this is a good thing, and it lines up very very nicely with things the community is already doing by default (e.g. right-biased Xor), but it needs to be made clear.
This comment has been minimized.
This comment has been minimized.
Blaisorblade
Apr 16, 2016
Contributor
it lines up very very nicely with things the community is already doing by default (e.g. right-biased Xor), but it needs to be made clear.
I'm guessing that's an heritage of Haskell's (here, from Either), where type inference works this way since ever. Hence, I conjecture no-Haskell Scalaers might especially need this explained.
This comment has been minimized.
This comment has been minimized.
larsrh
Apr 16, 2016
Contributor
I'm guessing that's an heritage of Haskell's (here, from
Either), where type inference works this way since ever.
Yes, but on top of that, in Haskell, left-to-right is the only thing you can do.
This comment has been minimized.
This comment has been minimized.
raulraja
commented
Apr 15, 2016
|
+1 a much awaited fix, this would be great to have in 2.12 |
This comment has been minimized.
This comment has been minimized.
|
@djspiewak the bug that @runarorama found, and a related one that @xuwei-k found are both fixed in this PR. |
This comment has been minimized.
This comment has been minimized.
|
I'm unreasonably excited about this! And if there is a chance to get this for 2.12 - oh my! What a start into the weekend. Awesome work @milessabin |
This comment has been minimized.
This comment has been minimized.
|
This causes lots of pain and leads to lots of weird magic tricks in the source code of Scala libraries that are extremely off-putting to new contributors. |
This comment has been minimized.
This comment has been minimized.
|
So, I personally like this idea (I implemented something similar in a branch of Dotty a while ago, but didn't pursue it further because of a million more urgent things), but there's a bunch of questions that need to be answered to go down that road, for example: class X1
class X2
class X3
trait One[A]
trait Two[A, B]
class Foo extends Two[X1, X2] with One[X3]
object Test {
def test[M[_], A](x: M[A]): M[A] = x
val foo = new Foo
test(foo) // M = ?, A = ?
}With However, this seems pretty bad: class X1
class X2
class X3
trait One[A]
trait Two[A, B]
class Foo extends Two[X1, X2] with One[X3]
object Test {
def test[M[X] <: One[X], A](x: M[A]): M[A] = x
val foo = new Foo
test(foo) // M = ?, A = ?
}With
in other words, |
This comment has been minimized.
This comment has been minimized.
|
@smarter Ouch! I'm assuming that it's trying |
This comment has been minimized.
This comment has been minimized.
|
@non: Yup. You can play with it easily using the sbt project at https://github.com/milessabin/si2712fix-demo |
This comment has been minimized.
This comment has been minimized.
|
@smarter It seems like this may just be a generalization of a pre-existing problem in Scala: class X1
class X3
trait One[A]
trait Three[A]
class Foo31 extends Three[X1] with One[X3]
object Test {
def test[M[X] <: One[X], A](x: M[A]): M[A] = x
val foo31 = new Foo31
test(foo31) // M = ?, A = ?
}With this I get:
This doesn't invalidate the point that there are some things that could stop type-checking. But I do think that it's consistent with the kinds of problems these constraints already have. |
This comment has been minimized.
This comment has been minimized.
|
Everyone on my team would very much like to see this in 2.12. |
This comment has been minimized.
This comment has been minimized.
ekmett
commented
Apr 15, 2016
|
|
This comment has been minimized.
This comment has been minimized.
wheaties
commented
Apr 15, 2016
|
|
This comment has been minimized.
This comment has been minimized.
@smarter The fact that this yields different results already tells us that we're in ambiguous/unspecified territory here. I think changing behaviour is acceptable under these circumstances. |
kailuowang
referenced this pull request
Apr 15, 2016
Closed
Error when the last field is a nested class #15
This comment has been minimized.
This comment has been minimized.
etorreborre
commented
Apr 15, 2016
|
@djspiewak what's the effect on Emm? |
This comment has been minimized.
This comment has been minimized.
|
@larsrh : changing behaviour is fine, but I don't think this can be done blindly: you need to think carefully about what semantics you want. |
This comment has been minimized.
This comment has been minimized.
djspiewak
commented
Apr 15, 2016
I get to delete about 90% of the boilerplate, with an exponential decrease in compile time due to the dramatically smaller search space. I still need the |
This comment has been minimized.
This comment has been minimized.
djspiewak
commented
Apr 15, 2016
•
|
I'm sure that @milessabin would explain things far better than I, but here's a quick write-up of how the fix works and what kind of implications it has for type constructor design: https://gist.github.com/7a81a395c461fd3a09a6941d4cd040f2 |
This comment has been minimized.
This comment has been minimized.
|
Big |
This comment has been minimized.
This comment has been minimized.
|
|
adriaanm
closed this
Apr 15, 2016
adriaanm
reopened this
Apr 15, 2016
This comment has been minimized.
This comment has been minimized.
|
Cool! Happy to have this go into M5 under a switch. I'm on vacation this week (hence the fat fingering on phone), but will take a look when I get back. |
This comment has been minimized.
This comment has been minimized.
|
As a final consideration, I'd like to revisit @smarter's comment above (#5102 (comment)). My understanding is that it's about the order of places where we look for a type constructor of the expected arity. Currently, there must be one in the base type sequence (transitive closure of the Is this the order we want? Should we look at the BTS for existing type constructors of the right arity before we start synthesizing new ones? PS: maybe it is the order we want, but I do think this change is important enough to double check and highlight in the release notes |
This comment has been minimized.
This comment has been minimized.
|
Slight OT: TL; DR. Unlike I thought, pattern unification is by far too weak for the interesting scenarios. So I figured I should really unsuggest it, and apologize a bit for suggesting it in the first place without doing enough homework, in particular to @milessabin and @mandubian. If anybody is interested in the evidence, see (and comment) this Agda snippet: |
adriaanm
added
the
release-notes
label
May 26, 2016
This comment has been minimized.
This comment has been minimized.
|
@milessabin I'll let @retronym do the honors, but I think this is ready to merge. Since this definitely merits inclusion in the release notes, could you update and expand the PR description to make it suitable for that? My last comment is probably one of the things that should be mentioned as a breaking change. |
This comment has been minimized.
This comment has been minimized.
retronym
changed the title
Add support for higher order unification. Fixes SI-2712.
SI-2712 Add support for partial unification of type constructors
May 27, 2016
This comment has been minimized.
This comment has been minimized.
|
Merging now, I'll let @milessabin update the PR description with a small example of what this means to users |
retronym
merged commit 6b2037a
into
scala:2.12.x
May 27, 2016
6 checks passed
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
|
Somewhat off-topic, but do you think that partial unification in implicit search could also have a similar solution? Example: object ImplicitSearchTest {
import scala.language.higherKinds
final case class KPair[F[_], G[_], A](_1: F[A], _2: G[A])
trait Getter[A, B] {
def get(a: A): B
}
implicit def leftProjection[F[_], G[_], A]: Getter[KPair[F, G, A], F[A]] =
new Getter[KPair[F, G, A], F[A]] {
def get(p: KPair[F, G, A]): F[A] = p._1
}
implicit def rightProjection[F[_], G[_], A]: Getter[KPair[F, G, A], G[A]] =
new Getter[KPair[F, G, A], G[A]] {
def get(p: KPair[F, G, A]): G[A] = p._2
}
implicit def composedRightProjection[F[_], G[_], A, B](implicit G: Getter[G[A], B]): Getter[KPair[F, G, A], B] =
new Getter[KPair[F, G, A], B] {
def get(p: KPair[F, G, A]): B = G.get(p._2)
}
trait Foo[A]
trait Bar[A]
trait Baz[A]
//type FooBarBaz[A] = KPair[Foo, KPair[Bar, Baz, ?], A]
type FooBarBaz[A] = KPair[Foo, ({ type Out[X] = KPair[Bar, Baz, X] })#Out, A]
implicitly[Getter[FooBarBaz[Int], Foo[Int]]] // error: could not find implicit value
implicitly[Getter[FooBarBaz[Int], Bar[Int]]] // error: could not find implicit value
implicitly[Getter[FooBarBaz[Int], Baz[Int]]] // error: could not find implicit value
// if we create an intermediate alias, implicit search works
type BarBaz[A] = KPair[Bar, Baz, A]
type FooBarBaz1[A] = KPair[Foo, BarBaz, A]
implicitly[Getter[FooBarBaz1[Int], Foo[Int]]] // OK
implicitly[Getter[FooBarBaz1[Int], Bar[Int]]] // OK
implicitly[Getter[FooBarBaz1[Int], Baz[Int]]] // OK
} |
This comment has been minimized.
This comment has been minimized.
|
@TomasMikula IIUC, if you expanded |
This comment has been minimized.
This comment has been minimized.
|
@Blaisorblade huh, the example actually compiles as is. I probably added Miles's |
This comment has been minimized.
This comment has been minimized.
|
I'm not sure the story is over, here are some examples which I think should compile with |
cb372
referenced this pull request
Aug 4, 2016
Merged
Poll the Apple endpoint to retrieve a report #1
milessabin
referenced this pull request
Aug 15, 2016
Merged
SI-2712 Add support for higher order unification #5343
philwills
referenced this pull request
Oct 7, 2016
Merged
Switch to ValidatedNel / Validated instead of Either #369
adriaanm
added
2.12
and removed
2.12
labels
Oct 29, 2016
This comment has been minimized.
This comment has been minimized.
|
Here's something to study: https://gist.github.com/paulp/71fa03ad85917f5fa02a3e8acbc98409 /cc @paulp |
This comment has been minimized.
This comment has been minimized.
djspiewak
commented
Mar 30, 2017
|
@adriaanm Wow… That definitely looks like a bug to me. Fabricating the |
This comment has been minimized.
This comment has been minimized.
|
Each extraneous factor I eliminate and it gets a little worse. It turns one doesn't need any compiler options at all - using the typelevel compiler is enough. I updated the gist to reflect this. |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
|
Here's a sliver of a diff after adding logging. The side which mentions Singleton is of course TLC. |
This comment has been minimized.
This comment has been minimized.
djspiewak
commented
Mar 31, 2017
|
A guess would be that a change made to ensure inference doesn't throw away specific singleton types quite so aggressively that was meant to be behind a flag… isn't. It sounds more related to the |
This comment has been minimized.
This comment has been minimized.
|
@djspiewak I think your guess will prove to be correct. |
This comment has been minimized.
This comment has been minimized.
|
It figures to be this, from 8301e1f. case argTp@SingletonInstanceCheck(cmpOp, cmpArg) if settings.YliteralTypes =>I'm supposing the side-effecting unapply is called, and then the settings guard is checked. |
This comment has been minimized.
This comment has been minimized.
|
Yup, this relates to the literal types PR rather than this one. |
milessabin commentedApr 15, 2016
•
edited
An improvement to type inference for type constructors has been added, enabled by the
-Ypartial-unificationcommand line option (also enabled in-Xexperimentalmode). This fixes SI-2712 in the way suggested by Paul Chiusano in a comment on the issue. This has many benefits for libraries, such as Cats and Scalaz, which make extensive use of higher-kinded types.With the feature enabled the following compiles,
with the type variable
F[t]solved asInt => tand the type variableAsolved asInt. Previously the compiler would have rejected this programme because,Foffoois a type constructor with a single type parameter, also known as kind * -> *.Int => Int, which is a synonym forFunction1[Int, Int], has an outer type constructor with two type parameters, or kind * -> * -> *.Partial unification solves this problem by treating outer type constructors at call sites as partially applied. In the example above, the compiler does the equivalent of creating a local type alias with the correct kind and using that at the call site,
Partialhas the same kind as the type argumentFoffooand so this compiles successfully.The implementation partially applies type constructors from left to right, leaving the rightmost type parameters free. This works well with binary type constructors with a natural right bias, such as
Either,Xorin Cats and Scalaz's disjunction. For example amapoperation defined with the signature illustrated below will naturally map over the value of the righthand type, corresponding to validity, whilst leaving the value of the lefthand type untouched,An article by Daniel Spiewak expands on the consequences of this strategy.
A major benefit of enabling this feature is that existing workarounds for SI-2712, such as shapeless's
Unpackand Cats and Scalaz'sUnapplyand theirUsuffixed operations are no longer necessary. This both simplifies code from the library implementor and users points of view, and also potentially reduces compile times by virtue of being implemented as a primitive component of type checking rather than being encoded via type level computation using implicits.Enabling this feature only affects type inference hence code compiled separately with the feature enabled and disabled is fully binary compatible. There is a compiler plugin which makes this feature available for Scala 2.11.x and 2.10.x.