Scala Wart: Conflating total destructuring with partial pattern-matching
See original GitHub issueOpening this issue, as suggested by Martin, to provide a place to discuss the individual warts brought up in the blog post Warts of the Scala Programming Language and the possibility of mitigating/fixing them in Dotty (and perhaps later in Scala 2.x). These are based on Scala 2.x behavior, which I understand Dotty follows closely, apologies in advance if it has already been fixed
The following example, also from Lincon Atkinson’s blog, compiles without warning and fails with an exception at runtime:
@ val (a, b, c) = if (true) "bar" else Some(10)
scala.MatchError: bar (of class java.lang.String)
$sess.cmd105$.<init>(cmd105.sc:1)
$sess.cmd105$.<clinit>(cmd105.sc:-1)
The basic problem here is that when Scala sees val (a, b, c) = ...
, it doesn’t
mean which of two things you mean:
- Help me extract the values from
...
, and help me check that it’s a tuple - Help me extract the values from
...
, and fail at runtime if it is not a tuple
Currently, it assumes the latter, in all cases.
That makes any sort of “destructuring assignment” unchecked, and thus extremely unsafe.
The above example at least happily fails with an exception, but the following
exhibits the same problem, but instead truncates your data silently, losing the
5
:
@ for((a, b) <- Seq(1 -> 2, 3 -> 4, 5)) yield a + " " + b
res107: Seq[String] = List("1 2", "3 4")
Though the following also fails with an exception:
@ Seq(1 -> 2, 3 -> 4, 5).map{case (a, b) => a + " " + b}
scala.MatchError: 5 (of class java.lang.Integer)
$sess.cmd108$.$anonfun$res108$1(cmd108.sc:1)
scala.collection.TraversableLike.$anonfun$map$1(TraversableLike.scala:234)
scala.collection.immutable.List.foreach(List.scala:389)
scala.collection.TraversableLike.map(TraversableLike.scala:234)
scala.collection.TraversableLike.map$(TraversableLike.scala:227)
scala.collection.immutable.List.map(List.scala:295)
$sess.cmd108$.<init>(cmd108.sc:1)
$sess.cmd108$.<clinit>(cmd108.sc:-1)
While interpretation #2 makes sense in match
blocks and partial-functions,
where you expect to “fall through” to the next handler if it doesn’t match, it
doesn’t make much sense in cases like this where there is nowhere to fall
through to.
The correct solution would look something like this:
-
By default, assume the user wants 1. “Help me extract the values from
...
, and help me check that it’s a tuple” -
Require a special keyword if the user wants 2. “Help me extract the values from
...
, and fail at runtime if it is not a tuple”
A possible syntax might be using case
, which Scala developers already
associate with partial functions and pattern matches:
for(case (a, b) <- Seq(1 -> 2, 3 -> 4, 5)) yield a + " " + b
case val (a, b, c) = if (true) "bar" else Some(10)
This would indicate that you want to perform an “partial fail at runtime”
match, and the earlier non-case
examples:
for((a, b) <- Seq(1 -> 2, 3 -> 4, 5)) yield a + " " + b
val (a, b, c) = if (true) "bar" else Some(10)
Could then verify that the pattern match is complete, otherwise fail at compile time.
PostScript:
I noticed here that the Scala language spec already has words in it that talk about irrefutable patterns, which seem to match what I want these for-comprehension and val-destructuring cases to require. Whether those words mean anything, I do not actually know
Yawar Amin has noted that in the Rust language, the cases which closely mirror those in Scala (let
destructuring, and while
-loop/if
destructuring) do have a requirement of the destructuring patterns being irrefutable https://doc.rust-lang.org/beta/book/second-edition/ch18-02-refutability.html
Here’s a 2.12 failure mode in for-comprehensions that’s nonsensical, and is fundamentally caused by this issue:
lihaoyi ~$ amm
Loading...
Welcome to the Ammonite Repl 0.9.7
(Scala 2.12.2 Java 1.8.0_112)
If you like Ammonite, please support our development at www.patreon.com/lihaoyi
@ for((a, b) <- Right((1, 2))) yield a + b
cmd0.sc:1: value withFilter is not a member of scala.util.Right[Nothing,(Int, Int)]
val res0 = for((a, b) <- Right((1, 2))) yield a + b
^
cmd0.sc:1: type mismatch;
found : Any
required: String
val res0 = for((a, b) <- Right((1, 2))) yield a + b
^
Compilation Failed
Issue Analytics
- State:
- Created 6 years ago
- Reactions:28
- Comments:12 (10 by maintainers)
Top GitHub Comments
This turned up in the scala/contributors gitter
The canonical form is
val 1 = 2
.