scala / scala3

The Scala 3 compiler, also known as Dotty.
https://dotty.epfl.ch
Apache License 2.0
5.81k stars 1.05k forks source link

Associated types become opaque with indirection #21416

Closed farnoy closed 2 weeks ago

farnoy commented 3 weeks ago

Compiler version

3.4.2, 3.5.1-RC2

Minimized code

trait First[T]:
  type AssociatedFirst

trait Second[T]:
  type AssociatedSecond

given [T](using first: First[T]): Second[T] with
  type AssociatedSecond = first.AssociatedFirst

case class Test1()

given First[Test1] with
  type AssociatedFirst = String

val first = summon[First[Test1]]
val second = summon[Second[Test1]]
val str1: first.AssociatedFirst = "test1"
val str2: second.AssociatedSecond = "test1"

Output

-- [E007] Type Mismatch Error: -------------------------------------------------
1 |val str2: second.AssociatedSecond = "test1"
  |                                    ^^^^^^^
  |                                    Found:    ("test1" : String)
  |                                    Required: second.AssociatedSecond
  |-----------------------------------------------------------------------------
  | Explanation (enabled by `-explain`)
  |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  |
  | Tree: "test1"
  | I tried to show that
  |   ("test1" : String)
  | conforms to
  |   second.AssociatedSecond
  | but none of the attempts shown below succeeded:
  |
  |   ==> ("test1" : String)  <:  second.AssociatedSecond
  |     ==> ("test1" : String)  <:  second.first.AssociatedFirst
  |       ==> String  <:  second.first.AssociatedFirst  = false
  |     ==> ("test1" : String)  <:  second.first.AssociatedFirst
  |       ==> String  <:  second.first.AssociatedFirst  = false
  |     ==> String  <:  second.AssociatedSecond
  |       ==> String  <:  second.first.AssociatedFirst  = false
  |       ==> String  <:  second.first.AssociatedFirst  = false
  |
  | The tests were made under the empty constraint
   -----------------------------------------------------------------------------
1 error found

Expectation

I expected the compiler to be able to resolve second.AssociatedSecond as String but it seems that the associated type does not reduce.

farnoy commented 3 weeks ago

This seems to work as expected, but I don't know enough to say if an extra type parameter affects the implicit search in practice (would it succeed in all cases you'd expect?)

given [T, K](using first: First[T] { type AssociatedFirst = K }): Second[T] with
  type AssociatedSecond = first.AssociatedFirst

This workaround uses a refinement type (?), but why does it help?

som-snytt commented 3 weeks ago

That's very clever. I can make a few banal observations which may be obvious.

The given First is a singleton, so its member type is known

val first: given_First_Test1.type = given_First_Test1

The given Second is a def of result type

    given class given_Second_T[T >: Nothing <: Any](using first: First[T])
       extends Object(), Second[given_Second_T.this.T] {
      T
      protected given val first: First[T]
      type AssociatedSecond = given_Second_T.this.first.AssociatedFirst
    }

where the First is not constrained. Its AssociatedFirst could be anything.

The second, constrained version is

    given class given_Second_T[T >: Nothing <: Any, K >: Nothing <: Any](using
      first:
        First[T]
          {
            type AssociatedFirst = K
          }
    ) extends Object(), Second[given_Second_T.this.T] {
      T
      K
      protected given val first: First[T]{type AssociatedFirst = K}
      type AssociatedSecond = given_Second_T.this.first.AssociatedFirst
    }

where the K is happily inferred

val second: given_Second_T[Test1, String] = ???

because AssociatedFirst is known. I don't know if you intend that the given First is always a singleton (unparameterized).

My experience is limited, but I expected this to be solvable by inlining. Is inlining not permitted for parameterized givens?

farnoy commented 3 weeks ago

I don't know if you intend that the given First is always a singleton (unparameterized).

That's right. In my real code, I generate a lot of types with scalameta and one given each, targeting the same trait/typeclass.

Is inlining not permitted for parameterized givens?

This wouldn't be practical in my case because I have thousands of auto-generated types and inlining is slow. And it doesn't seem to help:

inline given [T](using first: First[T]): Second[T] = new Second[T]:
  type AssociatedSecond = first.AssociatedFirst

inline given First[Test1] = new First[Test1]:
  type AssociatedFirst = String
dwijnand commented 2 weeks ago

Yeah, this is working as expected. You need to preserve more type information (the type of the returned given) if you want to rely on it.

farnoy commented 4 days ago

Isn't it strange that a non-implicit definition infers all of this automatically?

scala> def secondFromFirst[T](using first: First[T]) = new Second[T]:
     |
     |   type AssociatedSecond = first.AssociatedFirst
     |
def secondFromFirst
  [T]
    (using first: First[T]):
      Second[T]{type AssociatedSecond = first.AssociatedFirst}

Feels like a footgun

dwijnand commented 4 days ago

Perhaps you can get your auto-generated types to add those associated types to the given type:

given [T](using first: First[T]): (Second[T] {
    type AssociatedSecond = first.AssociatedFirst }) =
  new Second[T]:
    type AssociatedSecond = first.AssociatedFirst
farnoy commented 3 days ago

I did figure it out now with refinement types and it's been working fine. Just bothers me a little that it's not the default and you need to be aware of it.

In my case, the refinement is needed for the using parameter, so given [T, AssocFirst](using first: First[T] { type AssociatedFirst = AssocFirst }): Second[T]

I didn't need to refine the type of my given (so far, at least)

dwijnand commented 3 days ago

Yeah, type members don't need to be specified, unlike type arguments (for type parameters).