lampepfl / dotty-feature-requests

Historical feature requests. Please create new feature requests at https://github.com/lampepfl/dotty/discussions/new?category=feature-requests
31 stars 2 forks source link

Destructuring lambdas #207

Closed TomasMikula closed 1 year ago

TomasMikula commented 3 years ago

I propose to support destructuring lambdas, i.e. function definitions in which the parameter is deconstructed via a pattern.

Example:

case class Pair[A, B](_1: A, _2: B)

val f = Pair(a: Int, Pair(b: String, c: Boolean)) => s"($a, ($b, $c))"
// f: Pair[Int, Pair[String, Boolean]] => String

An important part of this feature is that the pattern would drive type inference: The type ascriptions a: Int, b: String, c: Boolean in the pattern above are not type tests; they are constraints for type inference/type checking. The compiler would infer the type of f to be Pair[Int, Pair[String, Boolean]] => String.

Workaround

The currently available workaround is to do a destructuring assignment as the first thing in the lambda body:

val f = { (t: Pair[Int, Pair[String, Boolean]]) =>
  val Pair(a, Pair(b, c)) = t
  s"($a, ($b, $c))"
}

We can see the repetitiveness as well as the need to introduce an intermediate name t.

LPTK commented 3 years ago

Probably a better workaround would be:

val f: Pair[Int, Pair[String, Boolean]] => String =
  Pair(a, Pair(b, c)) => s"($a, ($b, $c))"

But it does have the drawback of requiring a result type.

So I like your proposal, because it also seems to make the language more uniform. It would mean that tuple syntax is no longer special. At the very least, I think this should be accepted:

val f = { case Pair(x: Int, y: Int) => x }

Currently, even this is rejected:

val f = { case (x: Int) => x }
TomasMikula commented 3 years ago

Probably a better workaround would be:

val f: Pair[Int, Pair[String, Boolean]] => String =
  Pair(a, Pair(b, c)) => s"($a, ($b, $c))"

Well that doesn't compile, perhaps you meant

val f: Pair[Int, Pair[String, Boolean]] => String =
  { case Pair(a, Pair(b, c)) => s"($a, ($b, $c))" }

It would mean that tuple syntax is no longer special.

Note that destructuring in lambdas is not supported even for tuples:

val f = ((a: Int, b: String)) => s"($a, $b)"
1 |val f = ((a: Int, b: String)) => s"($a, $b)"
  |         ^^^^^^^^^^^^^^^^^^^
  |         not a legal formal parameter

At the very least, I think this should be accepted:

val f = { case Pair(x: Int, y: Int) => x }

I think the convention is that case means a runtime check. Then this code, if accepted, would define a PartialFunction[Any, Int], not Pair[Int, Int] => Int.

LPTK commented 3 years ago

Yes, I agree with your points, except the last one.

The convention that "case means a runtime check" does not really exist in practice. You can use case to define both normal and partial function literals, depending on the expected type. And since currently there is no way of writing case-defined lambdas without an expected type, there is no convention that case relates to runtime checks and PartialFunction in particular. IIRC the Scala spec does call case-defined lambdas "partial function literals" or something like that, but in practice there is no such distinction. On a related note, the typing of x => may differ from that of case x => because the latter triggers GADT reasoning (at least in Scala 2), so they are not interchangeable either.