Skip to content

Simple application of opaque types, extension methods hangs in runtime #11403

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

Closed
kubukoz opened this issue Feb 13, 2021 · 13 comments
Closed

Simple application of opaque types, extension methods hangs in runtime #11403

kubukoz opened this issue Feb 13, 2021 · 13 comments

Comments

@kubukoz
Copy link
Contributor

kubukoz commented Feb 13, 2021

Compiler version

3.0.0-M3

Minimized code

as scastie: https://scastie.scala-lang.org/3YbjmeaUTYWzwBOQOdRt8g

trait Eq[A]:
  def eqv(self: A, another: A): Boolean
  extension (self: A) def ===(another: A): Boolean = eqv(self, another)

object Eq:
  def apply[T: Eq]: Eq[T] = summon[Eq[T]]
  given Eq[String] = (self, another) => self == another

opaque type UserId = String
object UserId {
  def apply(value: String): UserId = value

  given Eq[UserId] =
    println("bar")
    Eq[String]
}

object Main:
  def main(args: Array[String]): Unit =
    println(UserId("foo") === UserId("foo"))

Output

bar
<hangs now>

Expectation

outputs bar, then true, then exits.

More context

This doesn't happen when Eq[UserId].eqv(..., ...) is used instead.

@Swoorup
Copy link

Swoorup commented Feb 13, 2021

@kubukoz duplicate of #10947

@kubukoz
Copy link
Contributor Author

kubukoz commented Feb 13, 2021

Why does it work properly when I manually summon the instance though?

@odersky
Copy link
Contributor

odersky commented Feb 14, 2021

Here's the translation of given Eq[UserId]:

      final lazy given val given_Eq_UserId: Eq[test$package.UserId] = 
        {
          println("bar")
          Eq.apply[String](UserId.given_Eq_UserId)
        }

It needs to find an instance for Eq[String] and finds one in scope: the instance for Eq[UserId]. In that scope
UserId and String are known to be aliases.

There's no ambiguity with the Eq[String] instance since in-scope implicits have higher-priority than implicits associated with a type.

@odersky odersky closed this as completed Feb 14, 2021
@kubukoz
Copy link
Contributor Author

kubukoz commented Feb 17, 2021

I still don't understand why this would cause an issue when in main I change UserId("foo") === UserId("foo") to Eq[UserId].eqv(UserId("foo"), UserId("foo")). This isn't in a location where opaque types are equivalent to the type being aliased.

@arturopala
Copy link
Contributor

It works when an instance of Eq[String] is not in Eq companion:

https://scastie.scala-lang.org/arturopala/whZCf9PuSwigZxjwc4Kw4w

trait Eq[A]:
  def eqv(self: A, another: A): Boolean
  extension (self: A) def ===(another: A): Boolean = eqv(self, another)

object Eq:
  def apply[T: Eq]: Eq[T] = summon[Eq[T]]

object Foo:
  given Eq[String] = (self, another) => self == another

opaque type UserId = String
object UserId {
  def apply(value: String): UserId = value

  given Eq[UserId] =
    println("bar")
    import Foo.given
    Eq[String]
}

object Main:
  def main(args: Array[String]): Unit =
    println(UserId("foo") === UserId("foo"))

@kubukoz
Copy link
Contributor Author

kubukoz commented Feb 17, 2021

That's... Even more surprising?

@smarter
Copy link
Member

smarter commented Feb 17, 2021

I still don't understand why this would cause an issue when in main I change UserId("foo") === UserId("foo") to Eq[UserId].eqv(UserId("foo"), UserId("foo")).

I just tried it and I couldn't reproduce that behavior, it loops in the same way (you can check what code is actually being generated by adding -Xprint:typer to yout scalacOptions).

It works when an instance of Eq[String] is not in Eq companion:

That's... Even more surprising?

Notice that in this example import Foo.given is used, the givens from that import take precedence over the givens from the enclosing scope (http://dotty.epfl.ch/docs/reference/changed-features/implicit-resolution.html), the original example also works if you write it with:

  given Eq[UserId] =
    import Eq.given // Give precedence to the givens in Eq
    println("bar")
    Eq[String]

(you can be even more explicit by writing import Eq.given Eq[String]).

@kubukoz
Copy link
Contributor Author

kubukoz commented Feb 17, 2021

just tried it and I couldn't reproduce that behavior, it loops in the same way

Ouch, that's true - I must have checked with some other changes in place when I last tried this way. Sorry for that.

givens from that import take precedence over the givens from the enclosing scope

Makes sense, thanks for explaining.

Is there anything in plans to support automatic derivation of instances for opaque types?

@smarter
Copy link
Member

smarter commented Feb 17, 2021

Is there anything in plans to support automatic derivation of instances for opaque types?

Nope, I don't think this has ever been really discussed, so you can try starting a thread in https://contributors.scala-lang.org/, I think it's an interesting thing to explore but it will require some careful thought, the first thing to do would be to carefully study the history of GeneralizedNewtypeDeriving and roles in GHC and see how much of that is relevant for us.

@kubukoz
Copy link
Contributor Author

kubukoz commented Feb 17, 2021

Sounds good :) I suppose this kind of feature could come in a later 3.x without breaking on the binary/tasty level?

@smarter
Copy link
Member

smarter commented Feb 17, 2021

Yep! (but before adding complexity to the language, it's also worth asking first if case class UserId(value: String) derives Eq isn't good enough, sure you get extra allocations but I'm not convinced that is a big deal: https://contributors.scala-lang.org/t/synthesize-constructor-for-opaque-types/4616/76?u=smarter)

@kubukoz
Copy link
Contributor Author

kubukoz commented Feb 17, 2021

I'm going to quote that post quite often from now on 😂 it's good to see "you might not need newtypes" coming from a compiler developer...

@smarter
Copy link
Member

smarter commented Feb 17, 2021

I mean, it seems that no one believes me so it's entirely possible I'm wrong 😄

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

5 participants