looking again, this variant would work:
xs.match unsafe
case y :: ys => ???
looking again, this variant would work:
xs.match unsafe
case y :: ys => ???
[I think I like the `unsafe` suggestion as an alternative for `checkedCast`, so I am going to try that out.]
unsafe
does not have to be a method, as long a it can be written with a dot in front. I.e. x.unsafe
has to work. Whether we define it as a method call or as a soft keyword in that position is secondary for me. There is precedence of keywords after '.'
, for instance .this
, .super
, .class
.
I agree with @sjrd, making this look like a method will just confuse people.
isInstanceOf
is a method, albeit one that behaves magically in implementation. Itās a function on the thing before the dot that returns a value.
If checkedCast
does something different when itās used as a scrutinee, and/or if itās not actually doing anything other than modifying the behavior of the pattern analysis, then itās not anything remotely like a method and giving it method syntax is just confusing.
(At one point we discussed making match
a magic method. That could make sense and this would not.)
IIUC the only reason for proposing method syntax is because the previously proposed syntax is not so ergonomic, and this would be more ergonomic.
I think a lot of things would be designed better if the process was framed in a clear problem-solution manner. That is, letās define clearly what problem(s) weāre trying to solve, and use that as our guide to explore the solution space.
Hereās my attempt:
Problem: Currently patterns can often lead to unexpected runtime errors. Scala is helped popularize the value of preventing runtime errors statically, and as other mainstream languages have now pushed the envelope even further, and developers expect it in more and more places, itās fitting to change this behavior and prevent such pattern match runtime errors statically as well. However, the current behavior is often desirable, so the language should allow the user to choose whether to pattern match in a safer or less safe way. The syntax for either mode of pattern testing should be ergonomic and intuitive. This is needed for both match
expressions, as well as val
(or destructuring) patterns. (TBD is this needed for case
statement sequences other than match
expressions, for anonymous function and partial function literals?)
Is that an accurate and complete description of the problem from scratch? And if yes can we sort of restart the discussion without all the assumptions that were made previously?
Thanks @bishabosha - I did not know that this was legal syntax and I have never used it as such:
Welcome to Scala 3.3.1 (17.0.8.1, Java OpenJDK 64-Bit Server VM).
Type in expressions for evaluation. Or try :help.
scala> 42.match { case x => x + 1 }
val res0: Int = 43
scala> 42.match
| case x => x + 1
|
val res1: Int = 43
Why is this legal? Was there a specific reason to make it look like a method? (I mean if
and while
is not a methodā¦)
There are some use cases at the reference doc.
The āscrutineeā used to be a postfix expression, but is now infix.
The doc mentions chained matches, but for me:
scala> 1 + 2 match { case 2 => 1 }
scala.MatchError: 3 (of class java.lang.Integer)
... 33 elided
scala> 1 + 2.match { case 2 => 1 }
val res1: Int = 2
Aha! So the reason is to, by utilizing that dot binds harder, avoid having to use parenthesis. Well, spontaneously I have used parenthesis in those situations to be clear about the evaluation order.
I strongly believe unsafe
has to be used after dot. I also think it should be a method, since that way it can have a doc comment that makes it more discoverable. But it does not need to be a method on Any
. How about we make it an extension method in compiletime
?
import compiletime.unsafe
val y :: ys = xs.unsafe
You arguably canāt be more explicit than this.
What if we created a new annotation that serves the purpose of opting into unsafe downcasting, and then the extension method adds this annotation - this already behaves as expected with @unchecked
:
extension [T <: Matchable](t: T)
inline def asCheckable: t.type @unchecked = t: @unchecked
val xs: List[Any] = List(1,2,3)
val y :: ys = xs.asCheckable // no warning!
Maybe not use an annotation, maybe you invent a new kind of type syntactically (dynamic Foo
?) - but I guess to satisfy @sjrd if its a method then the types must change
I like compiletime.unsafe
as it is explicit about intent and what to expect: things may blow up at runtime if you havenāt thought this through carefully. If it can be an extension method then its regular and explainable.
I generally agree with @sjrd that having it as a method looks really odd. All the other methods are normal signatures with magic implementations, this would be the only one with a magic signature that affects compileability or not. A lot of the normal things you would expect to be able to do with methods - store their return type in a val
, pass it around, wrap it in parens/identity-function, etc. - cannot be done with this magic method. That is very similar to the problem described in the motivating example, where the fact that the annotation is not a real type annotation ends up causing a whole bunch of weird syntactic edge cases, and so the .checkedCast
magic method doesnāt really solve the roots of the problem
Iām also not a fan of keywords that look like methods. I know .match
is a thing now, but I still would prefer that keywords look like syntax and methods look like methods
I think a big part of the problem is the way @unchecked
is used: itās used as a type ascription, when itās not really a type! That is not how annotations in any other language look like, Scala is the odd one out here. In fact, itās not how annotations work on most Scala language constructs either: annotations on classes, objects, methods, vals, etc. are typically prefix syntax of the form
@foo
def bar
@foo
val bar
@foo
class Bar
@foo
object Bar
Only annotations on expressions are weird, in that they are applied via type ascriptions, even though theyāre not really types
(bar: @foo) match {...}
What if we made annotations on expressions prefix syntax as well? That would make the motivating examples in the original post look like
@unchecked
xs match
case y :: ys => ...
@unchecked val y: Int = x
The exact name of the annotation can be arguably changed, but IMO these syntaxes look perfectly fine, better than any of the other examples given in the thread so far.
Prefix expression annotations would require a change to the Scala base syntax, but so do the other examples, and thereās one key difference: prefix expression annotations would make the language more regular rather than less, as we are removing (or deprecating) a special case āannotations on expressions are written as type ascriptions, where all other annotations are prefixā rather than adding a special case āhereās a new magic syntax that just works for this one specific use caseā
Even usage of an annotation here is fine: the rule āannotations do not affect typecheckingā still applies, as all the @unchecked
annotation does is remove a particular error case. In spirit, itās no different from @nowarn
with -Xfatal-warnings
, where the @nowarn
does not change how things are typechecked and only serves as a way to disable specific errors (compiler warnings with -Xfatal-warnings
) to allow more leniency.
So you mean that it could/should be like this?
@unsafe
xs match
case y :: ys => ...
@unsafe val y :: ys = xs
And perhaps that the compiler reuquires it? Or just a warning if not?
Yes thatās it! The exact name can be argued.
IMO @unchecked
is perfectly fine. @unsafe
also has other well-established meanings on the platform (e.g. sun.misc.Unsafe
), and in the broader programming commuinity (e.g. memory safety). Any name we choose will have some degree of overloaded meanings. @unchecked
as ācompile this lenientlyā seems fine, even if ālenientlyā can mean different things in different situations
Furthermore, I think this argument against @unchecked
is not quite correct:
Here, the type Int
actually does get checked at runtime, once you start using them: the JVM will throw ClassCastException
s on xs.head
, xs.map
or any other operations that use the value. Sure, it isnāt thrown exactly at the match
expression, but 99% of the time itāll get thrown soon after.
If we say @unchecked
generic types in pattern matching is a misnomer, we should say that .asInstanceOf[List[Int]]
is a misnomer, and .isInstanceOf[List[Int]]
. In fact any time you get a List[Int]
thereās no way you can ensure that whoever passed you that List[Int]
is not lying to you and itās actually a List[String]
But fundamentally these are all properties of the way the underlying platforms reify types - Both JVM and JS do this identically w.r.t. this issue, as do almost every other typed language (C# is maybe the only mainstream exception). Every other typed language with non-reified generics has the same failure mode: you can do a type test against a generic type with the wrong type parameter, and itāll be fine until later when it gets checked and fails at runtime with a ClassCastException
/TypeError
/etcā¦
Itās a problem, but itās a relatively well-understood problem that i donāt think we need to solve specially for the Scala language
We cannot possibly have a leading annotation on a match
. The syntax simply does not work that way.
We can have an annotation in front of a val
but it would be misleading. Annotations on definitions are for clients of the definition, but here it is an internal implementation detail. Concretely, if I see an @unchecked on a val I am led to believe that there is something unchecked about usages of the val
.
So it boils down to, for a reader of the syntax, to understand what exactly is unsafe:
Iām inclined to guess that a ādot unsafeā on the actual unsafe thing binds harder in the mind of the reader to the thing that gets dotted.
Is this true? val
s can already have all sorts of modifiers: lazy
, private
, abstract
, override
. Most of these are not visible to clients of the definition at all; only private
matters to callers. The rest are purely āinternalā concerns, perhaps of interest to subclasses but not to use sites
I donāt see why an unchecked
modifier (assuming we canāt make annotations look nice) would apply to use-sites any more than the existing modifiers which mostly are def-site concerns
+1 to not making it look like a method if it isnāt a method.
I think the argument against using a method have to do with the name. xs.unsafe
is indeed weird. In what sense is xs
unsafe? But xs.checked
or xs.checkedCast
is perfectly good, since it is xs
that is checked at runtime against the expected pattern(s). So maybe checked
instead of checkedCast
?
val y :: ys = xs.checked
xs.checked match
case y :: _ =>
checked
applies to the scrutinee and says that the value will be checked to fit with the expected type or pattern.
I am more convinced than before that is has to be a method.
Thatās intriguing and terrifying at the same time. It means I can already today have silently failing exhaustivity and refutability tests like this:
def sneaky(x: Any): x.type & @unchecked = x
sneaky(Nil) match
case x :: xs => ...
Maybe that should be one more reason to get rid of @unchecked? With a checked
or checkedCast
method we donāt have this problem.
I get your point, thanks. But I think there is something ambiguous still with just checked
to an uninformed reader of that code. Perhaps runtimeCheck
is better as a middle ground between the scary unsafe
and the ambiguous checked
? Perhaps it can be said to be deliberately verbose to make that unsafe thing stick out as in:
xs.runtimeCheck match
case y :: ys => ...
val y :: ys = xs.runtimeCheck
I mean most things are ācheckedā by the compiler, so thatās the assumption when reading the code. There is no indication that we intend to shift errors from compile time to runtime with just checked
.
(Itās not that I think checked
is āsuper badā ā but still an itch that it does not really convey the intent and what happens explicitly.)
This works today: (slightly modified from @bishabosha )
scala> extension [T](t: T) inline def runtimeChecked: t.type @unchecked = t: @unchecked
def runtimeChecked[T](t: T): t.type @unchecked
scala> val xs: Any = List(1,2,3)
val xs: Any = List(1, 2, 3)
scala> val y :: ys = xs.runtimeChecked
val y: Any = 1
val ys: List[Any] = List(2, 3)
but as expected a warning without .runtimeChecked
:
scala> val y :: ys = xs
1 warning found
-- Warning: -------------------------------------------------------------------------------------------------------------------
1 |val y :: ys = xs
| ^^
| pattern's type ::[Any] is more specialized than the right hand side expression's type Any
|
| If the narrowing is intentional, this can be communicated by adding `: @unchecked` after the expression,
| which may result in a MatchError at runtime.
val y: Any = 1
val ys: List[Any] = List(2, 3)
And it blows up at runtime without any warning if the match is unsuccessful:
scala> val y :: ys = (42: Any).runtimeChecked
scala.MatchError: 42 (of class java.lang.Integer)
... 35 elided
The definition of the extension method makes sense if the @unchecked
annotation is read as @uncheckedAtCompiletime
ā¦
So @odersky: as this works today; could we just introduce that extension method in the Scala 3 library and perhaps rename @unchecked to @uncheckedAtCompiletime or otherwise fix the annotation?