Pre SIP: Replace non-sensical @unchecked annotations

looking again, this variant would work:

xs.match unsafe
  case y :: ys => ???
1 Like

[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.

2 Likes

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?

1 Like

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

1 Like

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.

1 Like

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.

1 Like

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

3 Likes

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.

1 Like

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.

8 Likes

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 ClassCastExceptions 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

2 Likes

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.

2 Likes

So it boils down to, for a reader of the syntax, to understand what exactly is unsafe:

  1. is it the val declaration
  2. is it the ā€œscrutineeā€ that gets unapplied
  3. is it the specific pattern itself
  4. or the entire matching

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? vals 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

3 Likes

+1 to not making it look like a method if it isnā€™t a method.

2 Likes

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.

1 Like

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.

1 Like

(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?

1 Like