-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Nested transparent call does not refine the type #8739
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
Comments
Minimized to trait Nat
case object Zero extends Nat
case class Succ[N <: Nat](pred: N) extends Nat
transparent inline def s(inline y: Nat): Nat = Succ(y)
val d: Succ[Zero.type]= s(Zero) val d: Succ[Zero.type] =
{
Succ.apply[Nat]( // This type should be refined to Zero.type without modifying the initial elaboration
{
Zero
}
)
} |
This is a consequence of the fact that nested calls to transparent inline functions are typed with their declared return type, and thereby fix type inference in the enclosing method. After thinking hard about it I believe there's nothing we can do about this, short of going all the way back to untyped transparent functions, which are not semantics preserving and which can fail on expansion. We should be able to use match types to give a better type to these functions. Unfortunately, that does not work either: trait HList
case object HNil extends HList
case class HCons[H, T <: HList](h: H, t: T) extends HList
object test:
type Concat[Xs, Ys <: HList] <: HList = Xs match
case HNil.type => Ys
case HCons[x, xs1] => HCons[x, Concat[xs1, Ys]]
transparent inline def concat[Xs <: HList, Ys <: HList]
(inline xs: Xs, inline ys: Ys): Concat[Xs, Ys] =
inline xs match
case HNil => ys
case HCons(x, xs1) =>
val tail = concat(xs1, ys)
HCons(x, tail) gives
@OlivierBlanvillain do you see a way to fix this (either change the code, or improve the match type checking to accept the code?) |
An alternative strategy would be to make inferred types be more flexible, using some of the ideas of Georg and Olivier. The reason the transparent inline def concat(inline x: HList, inline y: HList): HList =
inline x match
case HNil => y
case HCons(h, t) =>
val tail: HList = concat(t, y)
HCons[h, Hlist](h, tail) // <--- HList is the problem here We could fix this if instead of the declared type of a transparent def we pick a type that refers to the call. E.g.
Then these TypeOfs can be expanded to the actual type of the expansion of the call, not the declared type. (In practice, the TypeOfs would just contain a pointer to the It does not stop there. We'd then also have to track these types through (at least) if-then-else, match expressions, and local bindings, which is what Georg and Olivier do(*). Basically, everything that depends on a TypeOf has to become a TypeOf. If we do this, there are still other questions:
In the TypeOf approach I believe the answer to both questions must be no, since we would push far too much complexity into the types. Overall we can now discern three possible strategies:
(*) In fact their work does this tracking also through recursive calls which means that they can compute precise TypeOf types even without expanding the function. The difference is whether TypeOf types There's also a difference how TypeOf is defined. In the scheme sketched here |
In fact, the transparent inline def f(): Any =
...
val h = g(f)
...
def g[X](x: X): X => X = ??? What is the type of A similar problem is for implicit search. Searching an implicit with I think the only feasible strategy is to own up to the TypeOf business immediately. I.e when typechecking a transparent function body, the result of a call to a transparent function Then we use these types normally in type-checking and type inference. This might produce errors when typechecking transparent functions. For instance, we might search an implicit of computed type This stricter type checking could be annoying but I believe it's better than the alternative. In essence, we tighten the rules for transparent functions to ensure that their expansions will not produce surprisingly weak types. That's better than accepting the transparent function as is and then asking the user to figure out why it did not produce the right type. |
Indeed, that's an interesting issue.
To clarify, when you say the "type computed normally" you mean Switching to Dotty-internal notation for a second, I'm a bit confused why a
Agreed, that sounds tricky. Have you considered the option that such singleton types not take part in implicit resolution at all, and we always use the upper bound ( |
The match type version compiles on master when using type test patterns (the only type of pattern that is currently expressible in match types): sealed trait HList
case class HCons[H, T <: HList](h: H, t: T) extends HList
sealed trait HNil extends HList
case object HNil extends HNil
object Test {
type Concat[Xs <: HList, Ys <: HList] <: HList = Xs match {
case HNil => Ys
case HCons[x, xs1] => HCons[x, Concat[xs1, Ys]]
}
def concat[Xs <: HList, Ys <: HList](xs: Xs, ys: Ys): Concat[Xs, Ys] =
xs match {
case _: HNil => ys
case c: HCons[x, xs1] =>
val tail = concat(c.t, ys)
HCons(c.h, tail)
}
} |
Ah, OK. Good that it works with type tests! Can we make it support other patterns as well? Or, alternatively, tell the user that only type tests are supported? Right now one is left in the dark why it fails. |
Yes that might work for certain types unapplies, such as the ones generated for case classes. It's already on the TODO list :)
We discarded the idea of giving verbose warnings in this case because would requires type checking the expression twice, first in "match type" mode to emit the warning, then fallback to normal mode if that didn't go through. |
I think that would not work. Here's a problematic example: class A
class B extends A
given a as A
def f[X](c: Boolean, x: X)(using y: X): X =
if c then x else y
transparent inline def foo(n: A): A =
val b = f(false, foo(n-1)) Here the call would expand to
If the recursive call
but that call will give an |
But we could do a simple pass on what the match was, and if it uses unsupported elements, tell the user, no? |
Ah, yes, that's a pretty glaring issue. I guess to achieve both soundness and legibility, we could just be honest about the type of What's our goal here anyways? Do we only want to maintain existing use cases of implicits, which would suddenly start failing, because some terms carry more precise types now? |
Does this limitation apply to types alone as well? I tried to overcome related issue #8779 by specifying the type manually as type MapRec[T <: Tuple, A, B] <: Tuple = T match {
case A *: tt => B *: MapRec[tt, A, B]
case t *: tt => t *: MapRec[tt, A, B]
case Unit => Unit
}
// Before:
// inline def mapRec[T <: Tuple, A, B](p:Product)(i:Int)(f: A => B) <: Tuple = ...
// After:
inline def mapRec[T <: Tuple, A, B](p:Product)(i:Int)(f: A => B) <: MapRec[T, A, B] = ... and this is what I got scala> mapRec[(Int, String), Int, Double]((1,"lol"),_.toDouble)(0)
val res0: Double *: MapRec[String *: Unit, Int, Double] = (1.0,lol) Is allowing the recursive type to be fully expanded as dificult as expanding the recursively inlined transparent function? EDIT: In the example above, the type |
I've only just become aware of this issue. It's a pretty enormous and terrible regression. I appreciate that it's non-trivial to fix, but the feature is essentially unusably broken in its current state. |
this also impacts the usefulness of |
From reading the discussion, it sounds like this is a very important feature, but I don't see an agreed upon solution. Is there a plan yet for how to tackle this, and when we could expect something? Thanks! |
I'm constantly hitting this, and it takes me a while to figure out what the problem is. Is there a way at least to generate a warning that this feature is not yet complete, if there is no viable solution in the near future? |
And I don't know if it's the exact same problem and solution, but maybe for the simple non-recursive cases there is also a more simple solution. trait Id[T]{type Out <: Int}
transparent inline def internal [T <: Int]: Id[T] = new Id[T]{type Out = T}
transparent inline def id: Int =
val one = internal[1]
??? : one.Out
val i : 1 = id //error |
@mbovel Not expecting a fix, but assigning to you for info, since the discussion is worth reading. |
Minimized code
Output
After typer the code is
Expectation
It should compile
The text was updated successfully, but these errors were encountered: