Skip to content

Fix comparison of dependent function types #12214

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

Merged
merged 6 commits into from
Apr 29, 2021

Conversation

odersky
Copy link
Contributor

@odersky odersky commented Apr 25, 2021

Dependent function types are expressed as refinements over regular function types.
These refinements need to be compared with the standard arrow rule for function
subtyping. But comparison of method refinements so far demanded equal parameter
types.

The solution is tricky since refined types lead to selections via reflection.
These still cannot tolerate differring parameter types since reflexive method dispatch
uses precise parameter types. That's why we apply the standard arrow rule only
for refinements where the refined method exists in the underlying class.

Fixes #12211

@odersky odersky requested a review from smarter April 25, 2021 11:52
@smarter
Copy link
Member

smarter commented Apr 25, 2021

The solution is tricky since refined types lead to selections via reflection.
These still cannot tolerate differring parameter types since reflexive method dispatch
uses precise parameter types.

Note that even with the current restriction in places, structural selection via reflection is unsound in both Scala 2 and 3:

import scala.language.reflectiveCalls

class Sink[A] { def put(x: A): Unit = {} }

object Test {
  def main(args: Array[String]): Unit = {
    val a = new Sink[String]
    val b: { def put(x: String): Unit } = a
    b.put("") // NoSuchMethodException: Sink.put(java.lang.String)
  }
}

So I would be in favor of allowing variance in structural types by default and phasing out reflectiveCalls in some way since it's broken beyond repair anyway, except that it's apparenty a big deal for Scala.js and I don't know what to do there /cc @sjrd.

@odersky
Copy link
Contributor Author

odersky commented Apr 25, 2021

Can we fix reflexive dispatch to deal with the problem? We do have the restriction that refinements may not be overloaded. That should help.

@smarter
Copy link
Member

smarter commented Apr 25, 2021

We do have the restriction that refinements may not be overloaded. That should help.

Only sort of, even if there's no overloading in Sink in my example above, nothing prevents a subclass from adding an overload. We could try to pick a best-effort overload but even that is tricky and not cross-platform as discussed in scala/bug#12297.

@odersky
Copy link
Contributor Author

odersky commented Apr 25, 2021

If we assume the no-overload restriction, we could fix applyDynamic if we can pass an additionional Class parameter representing the static parent class of the function type.

I.e if there is only one method, pick that. If there are several, pick the one that is defined in the parent class, or that overrides a method defined in the parent class.

@smarter
Copy link
Member

smarter commented Apr 25, 2021

we could fix applyDynamic if we can pass an additionional Class parameter representing the static parent class of the function type.

I don't follow, by the time we're typing b.put("") all we know about b is that it has a method put, but we don't know in which class it's defined (if we did know, then we wouldn't need to emit a structural call).

@odersky
Copy link
Contributor Author

odersky commented Apr 25, 2021

@smarter You are right. So there would always be cases where a reflexive dispatch will be ambiguous.

@odersky
Copy link
Contributor Author

odersky commented Apr 25, 2021

The other alternative would be to be stricter, disallowing

val b: { def put(x: String): Unit } = a

@odersky
Copy link
Contributor Author

odersky commented Apr 27, 2021

We now have a deluxe version of refinement subtyping, where the signature check can be disabled by inheriting from
scala.Selectable.WithoutPreciseParameterTypes.

* the additional restriction that the signatures of the refinement and
* the definition that implements the refinment must match.
*/
trait WithoutPreciseParameterTypes extends Selectable
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to be @experimental or commented out until 3.1

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved that part to a separate PR #12268 with a 3.1 milestone.

odersky added 6 commits April 29, 2021 13:47
Dependent function types are expressed as refinements over regular function types.
These refinements need to be compared with the standard arrow rule for function
subtyping. But comparison of method refinements so far demanded equal parameter
types.

The solution is tricky since refined types lead to selections via reflection
still cannot tolerate differing parameter types since reflexive method dispatch
uses precise parameter types. That's why we apply standard arrow rule only
for refinements where the refined method exists in the underlying class.

Fixes scala#12211
Needed to compile pos/i11481.scala
@odersky
Copy link
Contributor Author

odersky commented Apr 29, 2021

@smarter Ready to merge?

Copy link
Member

@smarter smarter left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, I'd like to extend this to allow polymorphic functions to vary but I can try that in a separate PR.

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

Successfully merging this pull request may close these issues.

Cannot create curried dependent functions
3 participants