Skip to content
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

Associated types become opaque with indirection #21416

Closed
farnoy opened this issue Aug 22, 2024 · 9 comments
Closed

Associated types become opaque with indirection #21416

farnoy opened this issue Aug 22, 2024 · 9 comments

Comments

@farnoy
Copy link

farnoy commented Aug 22, 2024

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 farnoy added itype:bug stat:needs triage Every issue needs to have an "area" and "itype" label labels Aug 22, 2024
@Gedochao Gedochao added area:typer and removed stat:needs triage Every issue needs to have an "area" and "itype" label labels Aug 23, 2024
@farnoy
Copy link
Author

farnoy commented Aug 28, 2024

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
Copy link
Contributor

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
Copy link
Author

farnoy commented Aug 29, 2024

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
Copy link
Member

dwijnand commented Sep 2, 2024

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.

@dwijnand dwijnand closed this as not planned Won't fix, can't repro, duplicate, stale Sep 2, 2024
@farnoy
Copy link
Author

farnoy commented Sep 15, 2024

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
Copy link
Member

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
Copy link
Author

farnoy commented Sep 15, 2024

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
Copy link
Member

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

@farnoy
Copy link
Author

farnoy commented Oct 8, 2024

I keep running into this regularly and also discovered #20061 through googling. With -language:experimental.modularity -source future, this solves the problem cleanly:

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants