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

Add unapply methods that return an Option for compatibility? #2335

Closed
smarter opened this issue Apr 30, 2017 · 18 comments
Closed

Add unapply methods that return an Option for compatibility? #2335

smarter opened this issue Apr 30, 2017 · 18 comments

Comments

@smarter
Copy link
Member

smarter commented Apr 30, 2017

Here's an interesting problem minmized from https://github.com/lampepfl/dotty/blob/master/compiler/src/dotty/tools/dotc/repl/ammonite/Protocol.scala that causes dotty-compiler-bootstrapped/repl to go into an infinite loop:

class Foo
case class Bar(x: Int) extends Foo

object Bar {
  def unapply(foo: Foo): Option[Int] = foo match {
    case foo: Bar =>
      unapply(foo)
  }
}

With scalac, the unapply call goes to the generated def unapply(x$1: Bar): Option[Int] , but with dotty no such method exist, instead we only generate a def unapply(x$1: Bar): Bar, so the previously perfectly correct code becomes an infinite loop!

I think we can fix this by always generating an unapply method that returns an Option, even if we don't use it.

@odersky
Copy link
Contributor

odersky commented May 1, 2017

Great that you got to the bottom of this! I think you meant:

case foo: Bar =>
  unapply(foo)

It turns out that neither this method nor the original method in Ammonite is needed. They just duplicate the typecheck that the compiler does anyway. One can and should delete them.

I am not very keen about generating a new method. It's ugly and will get us into overloading troubles. On the other hand, it certainly is a serious incompatibility with Scala 2.

@smarter
Copy link
Member Author

smarter commented May 1, 2017

I think you meant: ...

Yes, edited.

It turns out that neither this method nor the original method in Ammonite is needed. They just duplicate the typecheck that the compiler does anyway. One can and should delete them.

I tried to delete the unapply(ti: TermAction) in https://github.com/lampepfl/dotty/blob/master/compiler/src/dotty/tools/dotc/repl/ammonite/Protocol.scala and got an ambigous overload error because of the other unapply (unapply(ti: TermInfo)), removing that one breaks everything. Did you find a way to get it to compile?

@odersky
Copy link
Contributor

odersky commented May 1, 2017

I see. Well, in that case just inline TermState.unapply(ts) to Some(....)? I still think it's a corner case which should not force us to use an ugly compilation scheme.

@OlivierBlanvillain
Copy link
Contributor

OlivierBlanvillain commented May 1, 2017

Introducing an unapplyWorkaround method with the same signature that the scalac generated one does the trick.

OlivierBlanvillain added a commit to dotty-staging/dotty that referenced this issue May 1, 2017
OlivierBlanvillain added a commit to dotty-staging/dotty that referenced this issue May 1, 2017
@smarter
Copy link
Member Author

smarter commented May 1, 2017

@olafurpg Could you try to get some statistics on how often Scala projects explicitly use unapply ? (That is, unapply was written by the user and not generated by pattern-matching).

OlivierBlanvillain added a commit that referenced this issue May 1, 2017
@OlivierBlanvillain
Copy link
Contributor

I would expect the unapply usage to be very high. What we are interested in are direct calls to compiler generated unapply, which won't be so easy to differentiate from user defined ones...

In the common case the dotty/scalac difference would result in a compilation error (the return type of unapply changed from Option[TupleN] to A), this is a very sneaky example where it doesn't break compilation (because of overloading)...

@smarter I really don't see an alternative to your suggestion (maybe under -language:Scala2?), or snippets like the following won't cross compile:

case class A(i: Int, s: String)
val a: Option[(Int, String)] = A.unapply(A(1, "s"))

@odersky
Copy link
Contributor

odersky commented May 1, 2017

I'd say, let's wait and see how much breaks in practice.

@smarter
Copy link
Member Author

smarter commented May 1, 2017

I'd say, let's wait and see how much breaks in practice.

Sure.

I am not very keen about generating a new method. It's ugly and will get us into overloading troubles.

For future reference: I think we could get around overloading issue by marking the generated method as Bridge and preferring non-bridge to bridge in overloading resolution, I can't think of a case where this would change the current semantics.

@DarkDimius
Copy link
Contributor

@smarter, using Bridge flag for this use case is likely to break real bridge generation. It wont generate bridges to this bridge.

@smarter smarter changed the title Add unapply methods that return an Option for compatibility? (Blocker for the bootstrapped REPL) Add unapply methods that return an Option for compatibility? May 5, 2017
@odersky odersky removed the itype:bug label Jul 20, 2017
@jtjeferreira
Copy link
Contributor

This would help play-json cross compilation in code like this

case class Car(id: Long, models: Map[String, String])
implicit val CarFormat = (
  (__ \ Symbol("id")).format[Long] and
    (__ \ Symbol("models")).format[Map[String, String]]
)(Car.apply, unlift(Car.unapply))

Actually the Option could be dropped but unapply should return (Long, Map[Stringm String]) instead of Car...

@dwijnand
Copy link
Member

Let's try: Tuple.fromProductTyped(car)

@dwijnand
Copy link
Member

Indeed:

scala> case class Car(id: Long, models: Map[String, String])
// defined case class Car

scala> def unapply(car: Car) = Tuple.fromProductTyped(car)
def unapply(car: Car): (Long, Map[String, String])

@bishabosha
Copy link
Member

bishabosha commented Jan 11, 2021

scala> def unapply(car: Car) = Tuple.fromProductTyped(car)
def unapply(car: Car): (Long, Map[String, String])

I think this secretly returns a hidden generic tuple type leading to #10958

@xuwei-k
Copy link
Contributor

xuwei-k commented May 22, 2021

@dwijnand
Copy link
Member

Well, we missed the train on this for 3.0.0 and it hasn't seemed to be a major issue. Let's not do this.

@OlegYch
Copy link
Contributor

OlegYch commented Jul 28, 2022

uh, excuse me, what was the point of replacing previously useful unapply with identity?

@dwijnand
Copy link
Member

It allowed us to generalise cheap, option-less unapply methods to all Product types, not just case classes.

@ornicar
Copy link

ornicar commented Nov 10, 2022

In case someone else comes here looking for the generated case class unapply function, here's how I replaced it:

  def unapply[P <: Product](p: P)(using m: scala.deriving.Mirror.ProductOf[P]): Option[m.MirroredElemTypes] =
    Some(Tuple.fromProductTyped(p))

Now you can just use unapply where you would previously use Foo.unapply

rtyley added a commit to guardian/cross-platform-navigation that referenced this issue Sep 5, 2024
This is prompted by guardian/mobile-apps-api#3198 - we would like
to be able to upgrade MAPI to Scala 3. As MAPI depends on https://github.com/guardian/cross-platform-navigation,
it's good to have a Scala 3 version of this library.

The only tricky part of upgrading `cross-platform-navigation` to Scala 3 is handling the
explicit call to the `unapply()` method, which _used_ to return an `Option[(...,...,...)]`
with all the parameters used to create the case class - in Scala 3 that's changed:

* https://docs.scala-lang.org/scala3/guides/migration/incompat-other-changes.html#explicit-call-to-unapply
* scala/scala3#2335

The least magical way to cope with the changed `unapply()` method is to manually write out
what the unapply method used to produce, and that's what I've done here. Looking at the
discussion on the Scala issue, there are other workarounds which are Scala 3-only (and so
wouldn't easily work in this project, which cross-compiles with Scala 2):

* scala/scala3#2335 (comment) gives us this, which
  did work for me, but just in Scala 3:
  ```
  def doOldUnapply(ns: NavigationSection) = Some(Tuple.fromProductTyped(ns))
  ```
* scala/scala3#2335 (comment) - this is apparently
  a general version of the method above, but didn't work for me - complained that
  `NavigationSection` is not a `Product`, I think?
rtyley added a commit to guardian/cross-platform-navigation that referenced this issue Sep 5, 2024
This is prompted by guardian/mobile-apps-api#3198 - we would like
to be able to upgrade MAPI to Scala 3. As MAPI depends on https://github.com/guardian/cross-platform-navigation,
it's good to have a Scala 3 version of this library.

The only tricky part of upgrading `cross-platform-navigation` to Scala 3 is handling the
explicit call to the `unapply()` method, which _used_ to return an `Option[(...,...,...)]`
with all the parameters used to create the case class - in Scala 3 that's changed:

* https://docs.scala-lang.org/scala3/guides/migration/incompat-other-changes.html#explicit-call-to-unapply
* scala/scala3#2335

The least magical way to cope with the changed `unapply()` method is to manually write out
what the unapply method used to produce, and that's what I've done here. Looking at the
discussion on the Scala issue, there are other workarounds which are Scala 3-only (and so
wouldn't easily work in this project, which cross-compiles with Scala 2):

* scala/scala3#2335 (comment) gives us this, which
  did work for me, but just in Scala 3:
  ```
  def doOldUnapply(ns: NavigationSection) = Some(Tuple.fromProductTyped(ns))
  ```
* scala/scala3#2335 (comment) - this is apparently
  a general version of the method above, but didn't work for me - complained that
  `NavigationSection` is not a `Product`, I think?
rtyley added a commit to guardian/cross-platform-navigation that referenced this issue Sep 5, 2024
This is prompted by guardian/mobile-apps-api#3198 - we would like
to be able to upgrade MAPI to Scala 3. As MAPI depends on https://github.com/guardian/cross-platform-navigation,
it's good to have a Scala 3 version of this library.

The only tricky part of upgrading `cross-platform-navigation` to Scala 3 is handling the
explicit call to the `unapply()` method, which _used_ to return an `Option[(...,...,...)]`
with all the parameters used to create the case class - in Scala 3 that's changed:

* https://docs.scala-lang.org/scala3/guides/migration/incompat-other-changes.html#explicit-call-to-unapply
* scala/scala3#2335

The least magical way to cope with the changed `unapply()` method is to manually write out
what the unapply method used to produce, and that's what I've done here. Looking at the
discussion on the Scala issue, there are other workarounds which are Scala 3-only (and so
wouldn't easily work in this project, which cross-compiles with Scala 2):

* scala/scala3#2335 (comment) gives us this, which
  did work for me, but just in Scala 3:
  ```
  def doOldUnapply(ns: NavigationSection) = Some(Tuple.fromProductTyped(ns))
  ```
* scala/scala3#2335 (comment) - this is apparently
  a general version of the method above, but didn't work for me - complained that
  `NavigationSection` is not a `Product`, I think?
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