-
Notifications
You must be signed in to change notification settings - Fork 3.1k
SI-2712 Add support for partial unification of type constructors #5102
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3131,13 +3131,43 @@ trait Types | |
| */ | ||
| def unifyFull(tpe: Type): Boolean = { | ||
| def unifySpecific(tp: Type) = { | ||
| sameLength(typeArgs, tp.typeArgs) && { | ||
| val lhs = if (isLowerBound) tp.typeArgs else typeArgs | ||
| val rhs = if (isLowerBound) typeArgs else tp.typeArgs | ||
| val tpTypeArgs = tp.typeArgs | ||
| val arityDelta = compareLengths(typeArgs, tpTypeArgs) | ||
| if (arityDelta == 0) { | ||
| val lhs = if (isLowerBound) tpTypeArgs else typeArgs | ||
| val rhs = if (isLowerBound) typeArgs else tpTypeArgs | ||
| // This is a higher-kinded type var with same arity as tp. | ||
| // If so (see SI-7517), side effect: adds the type constructor itself as a bound. | ||
| isSubArgs(lhs, rhs, params, AnyDepth) && { addBound(tp.typeConstructor); true } | ||
| } | ||
| isSubArgs(lhs, rhs, params, AnyDepth) && {addBound(tp.typeConstructor); true} | ||
| } else if (settings.YpartialUnification && arityDelta < 0 && typeArgs.nonEmpty) { | ||
| // Simple algorithm as suggested by Paul Chiusano in the comments on SI-2712 | ||
| // | ||
| // https://issues.scala-lang.org/browse/SI-2712?focusedCommentId=61270 | ||
| // | ||
| // Treat the type constructor as curried and partially applied, we treat a prefix | ||
| // as constants and solve for the suffix. For the example in the ticket, unifying | ||
| // M[A] with Int => Int this unifies as, | ||
| // | ||
| // M[t] = [t][Int => t] --> abstract on the right to match the expected arity | ||
| // A = Int --> capture the remainder on the left | ||
| // | ||
| // 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 commentThe 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I'm guessing that's an heritage of Haskell's (here, from
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yes, but on top of that, in Haskell, left-to-right is the only thing you can do. |
||
| val numCaptured = tpTypeArgs.length - typeArgs.length | ||
| val (captured, abstractedArgs) = tpTypeArgs.splitAt(numCaptured) | ||
|
|
||
| val (lhs, rhs) = | ||
| if (isLowerBound) (abstractedArgs, typeArgs) | ||
| else (typeArgs, abstractedArgs) | ||
|
|
||
| isSubArgs(lhs, rhs, params, AnyDepth) && { | ||
| val tpSym = tp.typeSymbolDirect | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is lossy: the prefix of the That said, it appears we have the same problem with regular inference, so we can open a ticket to followup and fix both cases: A fix would be something along the lines of: |
||
| val abstractedTypeParams = tpSym.typeParams.drop(numCaptured).map(_.cloneSymbol(tpSym)) | ||
|
|
||
| addBound(PolyType(abstractedTypeParams, appliedType(tp.typeConstructor, captured ++ abstractedTypeParams.map(_.tpeHK)))) | ||
| true | ||
| } | ||
| } else false | ||
| } | ||
| // The type with which we can successfully unify can be hidden | ||
| // behind singleton types and type aliases. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| t2712-1.scala:7: error: no type parameters for method foo: (m: M[A])Unit exist so that it can be applied to arguments (test.Two[Int,String]) | ||
| --- because --- | ||
| argument expression's type is not compatible with formal parameter type; | ||
| found : test.Two[Int,String] | ||
| required: ?M[?A] | ||
| def test(ma: Two[Int, String]) = foo(ma) // should fail with -Ypartial-unification *disabled* | ||
| ^ | ||
| t2712-1.scala:7: error: type mismatch; | ||
| found : test.Two[Int,String] | ||
| required: M[A] | ||
| def test(ma: Two[Int, String]) = foo(ma) // should fail with -Ypartial-unification *disabled* | ||
| ^ | ||
| two errors found |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| package test | ||
|
|
||
| trait Two[A, B] | ||
|
|
||
| object Test { | ||
| def foo[M[_], A](m: M[A]) = () | ||
| def test(ma: Two[Int, String]) = foo(ma) // should fail with -Ypartial-unification *disabled* | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| t2712-2.scala:16: error: type mismatch; | ||
| found : test.Foo | ||
| required: test.Two[test.X1,Object] | ||
| Note: test.X2 <: Object (and test.Foo <: test.Two[test.X1,test.X2]), but trait Two is invariant in type B. | ||
| You may wish to define B as +B instead. (SLS 4.5) | ||
| test1(foo): One[X3] // fails with -Ypartial-unification enabled | ||
| ^ | ||
| t2712-2.scala:16: error: type mismatch; | ||
| found : test.Two[test.X1,Object] | ||
| required: test.One[test.X3] | ||
| test1(foo): One[X3] // fails with -Ypartial-unification enabled | ||
| ^ | ||
| two errors found |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| -Ypartial-unification |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| package test | ||
|
|
||
| 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 test1[M[_], A](x: M[A]): M[A] = x | ||
|
|
||
| val foo = new Foo | ||
|
|
||
| test1(foo): One[X3] // fails with -Ypartial-unification enabled | ||
| test1(foo): Two[X1, X2] // fails without -Ypartial-unification | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| t2712-3.scala:17: error: type mismatch; | ||
| found : test.One[test.X3] | ||
| required: test.Two[test.X1,test.X2] | ||
| test1(foo): Two[X1, X2] // fails without -Ypartial-unification | ||
| ^ | ||
| one error found |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| package test | ||
|
|
||
| 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 test1[M[_], A](x: M[A]): M[A] = x | ||
|
|
||
| val foo = new Foo | ||
|
|
||
| test1(foo): One[X3] // fails with -Ypartial-unification enabled | ||
| test1(foo): Two[X1, X2] // fails without -Ypartial-unification | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| -Ypartial-unification |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| -Ypartial-unification |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| package test | ||
|
|
||
| // Original test case from, | ||
| // | ||
| // https://issues.scala-lang.org/browse/SI-2712 | ||
| object Test { | ||
| def meh[M[_], A](x: M[A]): M[A] = x | ||
| meh{(x: Int) => x} // solves ?M = [X] Int => X and ?A = Int ... | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| -Ypartial-unification | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| package test | ||
|
|
||
| // See: https://github.com/milessabin/si2712fix-demo/issues/3 | ||
| object Test { | ||
| trait A[T1, T2] { } | ||
| trait B[T1, T2] { } | ||
| class C[T] extends A[T, Long] with B[T, Double] | ||
| class CB extends A[Boolean, Long] with B[Boolean, Double] | ||
|
|
||
| trait A2[T] | ||
| trait B2[T] | ||
| class C2[T] extends A2[T] with B2[T] | ||
| class CB2 extends A2[Boolean] with B2[Boolean] | ||
|
|
||
| def meh[M[_], A](x: M[A]): M[A] = x | ||
|
|
||
| val m0 = meh(new C[Boolean]) | ||
| m0: C[Boolean] | ||
| val m1 = meh(new CB) | ||
| m1: A[Boolean, Long] | ||
| val m2 = meh(new C2[Boolean]) | ||
| m2: C2[Boolean] | ||
| val m3 = meh(new CB2) | ||
| m3: A2[Boolean] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| -Ypartial-unification | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| package test | ||
|
|
||
| object Test1 { | ||
| class Foo[T, F[_]] | ||
| def meh[M[_[_]], F[_]](x: M[F]): M[F] = x | ||
| meh(new Foo[Int, List]) // solves ?M = [X[_]]Foo[Int, X[_]] ?A = List ... | ||
| } | ||
|
|
||
| object Test2 { | ||
| trait TC[T] | ||
| class Foo[F[_], G[_]] | ||
| def meh[G[_[_]]](g: G[TC]) = ??? | ||
| meh(new Foo[TC, TC]) // solves ?G = [X[_]]Foo[TC, X] | ||
| } | ||
|
|
||
| object Test3 { | ||
| trait TC[F[_]] | ||
| trait TC2[F[_]] | ||
| class Foo[F[_[_]], G[_[_]]] | ||
| new Foo[TC, TC2] | ||
|
|
||
| def meh[G[_[_[_]]]](g: G[TC2]) = ??? | ||
| meh(new Foo[TC, TC2]) // solves ?G = [X[_[_]]]Foo[TC, X] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| -Ypartial-unification | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| package test | ||
|
|
||
| object Test1 { | ||
| trait X | ||
| trait Y extends X | ||
| class Foo[T, U <: X] | ||
| def meh[M[_ <: A], A](x: M[A]): M[A] = x | ||
| meh(new Foo[Int, Y]) | ||
| } | ||
|
|
||
| object Test2 { | ||
| trait X | ||
| trait Y extends X | ||
| class Foo[T, U >: Y] | ||
| def meh[M[_ >: A], A](x: M[A]): M[A] = x | ||
| meh(new Foo[Int, X]) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| -Ypartial-unification |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| package test | ||
|
|
||
| import scala.language.higherKinds | ||
|
|
||
| trait Functor[F[_]] { | ||
| def map[A, B](f: A => B, fa: F[A]): F[B] | ||
| } | ||
|
|
||
| object Functor { | ||
| implicit def function[A]: Functor[({ type l[B] = A => B })#l] = | ||
| new Functor[({ type l[B] = A => B })#l] { | ||
| def map[C, B](cb: C => B, ac: A => C): A => B = cb compose ac | ||
| } | ||
| } | ||
|
|
||
| object FunctorSyntax { | ||
| implicit class FunctorOps[F[_], A](fa: F[A])(implicit F: Functor[F]) { | ||
| def map[B](f: A => B): F[B] = F.map(f, fa) | ||
| } | ||
| } | ||
|
|
||
| object Test { | ||
|
|
||
| val f: Int => String = _.toString | ||
|
|
||
| import FunctorSyntax._ | ||
|
|
||
| f.map((s: String) => s.reverse) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| -Ypartial-unification |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| package test | ||
|
|
||
| object Tags { | ||
| type Tagged[A, T] = {type Tag = T; type Self = A} | ||
|
|
||
| type @@[T, Tag] = Tagged[T, Tag] | ||
|
|
||
| trait Disjunction | ||
|
|
||
| def meh[M[_], A](ma: M[A]): M[A] = ma | ||
| meh(null.asInstanceOf[Int @@ Disjunction]) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| -Ypartial-unification |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| package test | ||
|
|
||
| // Cats Xor, Scalaz \/, scala.util.Either | ||
| sealed abstract class Xor[+A, +B] extends Product with Serializable | ||
| object Xor { | ||
| final case class Left[+A](a: A) extends (A Xor Nothing) | ||
| final case class Right[+B](b: B) extends (Nothing Xor B) | ||
| } | ||
|
|
||
| object TestXor { | ||
| import Xor._ | ||
| def meh[F[_], A, B](fa: F[A])(f: A => B): F[B] = ??? | ||
| meh(new Right(23): Xor[Boolean, Int])(_ < 13) | ||
| meh(new Left(true): Xor[Boolean, Int])(_ < 13) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| -Ypartial-unification |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| object Test { | ||
| trait NT[X] | ||
| trait W[W, A] extends NT[Int] | ||
| type StringW[T] = W[String, T] | ||
| trait K[M[_], A, B] | ||
|
|
||
| def k[M[_], B](f: Int => M[B]): K[M, Int, B] = null | ||
|
|
||
| val okay1: K[StringW,Int,Int] = k{ (y: Int) => null: StringW[Int] } | ||
| val okay2 = k[StringW,Int]{ (y: Int) => null: W[String, Int] } | ||
|
|
||
| val crash: K[StringW,Int,Int] = k{ (y: Int) => null: W[String, Int] } | ||
|
|
||
| // remove `extends NT[Int]`, and the last line gives an inference error | ||
| // rather than a crash. | ||
| // test/files/pos/t5683.scala:12: error: no type parameters for method k: (f: Int => M[B])Test.K[M,Int,B] exist so that it can be applied to arguments (Int => Test.W[String,Int]) | ||
| // --- because --- | ||
| // argument expression's type is not compatible with formal parameter type; | ||
| // found : Int => Test.W[String,Int] | ||
| // required: Int => ?M[?B] | ||
| // val crash: K[StringW,Int,Int] = k{ (y: Int) => null: W[String, Int] } | ||
| // ^ | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| -Ypartial-unification | ||
|
|
Uh oh!
There was an error while loading. Please reload this page.
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.
To summarize my feedback from the individual comments, here's
unifySpecificwith my suggestions applied: