-
Notifications
You must be signed in to change notification settings - Fork 3.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
SI-2712 Add support for partial unification of type constructors #5102
Conversation
+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! |
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. |
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. |
@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. |
Here is scalaz result. I can remove some |
// A = Int | ||
// | ||
// A more "natural" unifier might be M[t] = [t][t => t]. There's lots of scope for | ||
// experimenting with alternatives here. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
+1 a much awaited fix, this would be great to have in 2.12 |
@djspiewak the bug that @runarorama found, and a related one that @xuwei-k found are both fixed in this PR. |
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 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. |
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, |
@smarter Ouch! I'm assuming that it's trying |
@non: Yup. You can play with it easily using the sbt project at https://github.com/milessabin/si2712fix-demo |
@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. |
Everyone on my team would very much like to see this in 2.12. |
👍 This would greatly simplify a lot of scala code we have |
👍 for this but have to agree with @djspiewak on a post explaining how things will work, espcially in light of what @smarter has put up there. |
@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. |
@djspiewak what's the effect on Emm? |
@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. |
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 |
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 |
Big 👍 for inclusion in 2.12 ... this would make my life much happier. |
👍 thank you @milessabin |
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. |
Agreed on all counts (though I don't think you intended to remove the check for the presence of the enabling flag ;-) ). Updated and rebased. |
Getting close! Dim house lights, cue spotlight, drum roll... 😯 |
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 |
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: |
@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. |
Merging now, I'll let @milessabin update the PR description with a small example of what this means to users |
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
} |
@TomasMikula IIUC, if you expanded |
@Blaisorblade huh, the example actually compiles as is. I probably added Miles's |
I'm not sure the story is over, here are some examples which I think should compile with |
Here's something to study: https://gist.github.com/paulp/71fa03ad85917f5fa02a3e8acbc98409 /cc @paulp |
@adriaanm Wow… That definitely looks like a bug to me. Fabricating the |
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. |
Here's a sliver of a diff after adding logging. The side which mentions Singleton is of course TLC. |
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 |
@djspiewak I think your guess will prove to be correct. |
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. |
Yup, this relates to the literal types PR rather than this one. |
An improvement to type inference for type constructors has been added, enabled by the
-Ypartial-unification
command line option (also enabled in-Xexperimental
mode). 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 => t
and the type variableA
solved asInt
. Previously the compiler would have rejected this programme because,F
offoo
is 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,
Partial
has the same kind as the type argumentF
offoo
and 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
,Xor
in Cats and Scalaz's disjunction. For example amap
operation 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
Unpack
and Cats and Scalaz'sUnapply
and theirU
suffixed 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.