Pre-SIP: deprecate asInstanceOf, introduce unsafeAsInstanceOf

apparently not everyone knows it and asInstanceOf is a pitfall from time to time.
Also to be more precise, checked casts are safe in Java only unchecked casts are unsafe

1 Like

surely this will always fail, (i.e. it is not a matching type, unless you want it to not warn for List(1).asInstanceOf[List[String]]?) that’s why you need the cast in the first place (please use : Foo for type widening)

Agree 100%.

I understand that asInstanceOf is meant as an “escape hatch”. But I still think it should warn on obviously failing casts.

A beginner may for example try to “convert” a String to an Int by writing "23".asInstaceOf[Int]. The compiler should give a helpful warning in this case. Explaining what asInsaceOf is good for, and that in most cases something other is actually the proper desired solution. (E.g. “to*”-methods, or type ascriptions for widening).

(Maybe) getting the JVM cast exception at that point when running the code is more confusing imho (and requires to actually run that code path, which is not a given). And when you don’t get a cast exception directly at the wrong cast but later on things are usually quite difficult to debug.

For the cases where the compiler can definitely know that things are going to crash at runtime it should in my opinion tell the user upfront. With a nice explanation, and note on the proper use of asInstanceOf.

2 Likes

Proposed illustrative implementation from the related thread:

def asInstanceOf[A]: A = :troll:

There has always been (since forever) a drumbeat about asInstanceOf. If education and documentation have failed, why isn’t it behind a SIP-18 import?

There is little effective documentation for asInstanceOf.

  1. Most people won’t look at the spec (which is wrong anyways).
  2. My version of IntelliJ, which is up to date and working otherwise, does not provide any documentation for asInstanceOf, and I can imagine that many other Scala developers don’t have immediate access to it, and might not even be aware that it exists, given how hard it is to find online.
  3. Googling ‘scala asinstanceof’ does not return any official documentation in the top results.

I can imagine that a dedicated documentation page, with several examples and pitfalls, could be quite helpful to many people.

4 Likes

tried to improve the docs: make it clearer that asInstanceOf is unsafe and the outcome is undefined by robstoll · Pull Request #10696 · scala/scala · GitHub

3 Likes

I also checked and Metals does not provide a hover (in scala 2.13 and 3.3.1 where I tested) for asInstanceOf either

1 Like

maybe call it unsafeAsKnives[A].

Really it’s unsafeWhenNotInstanceOf. When it’s instanceof, it’s perfectly fine.

1 Like

I agree that fleshing out the docs, fixing the scaladoc in IDEs, etc. is worthwhile. IntelliJ doesn’t properly jump to definition or show Scaladoc too IIRC (not sure if that changed in some recent version). We should fix that. We can add pages on scala-lang.org to help with SEO. I just don’t think changing the name is the right answer here.

Though I wouldn’t be opposed to calling it .__AS_INSTANCE_OF_DO_NOT_USE_OR_YOU_WILL_BE_FIRED[T]!

3 Likes

Well that’s the thing with safety. If someone crosses a highway safely that doesn’t mean that crossing highways is a safe thing to do.

3 Likes

I agree with the other comments that regarding naming the ship has likely sailed.

But if I had to name it today I’d call it unsafelyAssumeType[?]. Because the “as” in the current name points indeed a little bit in the direction of a regular “conversion” method. Usually asSomething Methods are perfectly safe; and actually perform some kind of conversion. But asInstanceOf does not! You just tell the compiler that “you know better”, and it should assume you’re right, no questions asked (and especially no conversion inserted).

assInstanceOf. Just like “assume” makes an “ass” of “u” and “me”, it makes an instance of an ass. (Usually we can’t tell unless a good friend and co-worker points it out to us.)

1 Like

There’s been many interesting points made and I’d like to address some of those. So I’ll write multiple posts only so that people can more easily agree or disagree with each of them separately.

I can only speak for myself, but yes. I did read this documentation and FWIW I understood what it does. It helps that people coming with experience of a language that has UB at every corner won’t be shocked by the lack of guarantees.

IMO, as for many other elements of the documentation, the way to make it better is:

  1. Make sure that one of the top 3 Google results when you type “Scala asInstanceOf” on a fresh browser without cookies leads to an official source of documentation.
  2. Have a dedicated page for each method. On scala-lang.org you need to click on the method to see the additional notes. I would MUCH prefer a layout like this.
  3. Better explain why one would use the method and when one should not use it.

Specifically on asInstanceOf, having everything written in a single paragraph is slightly discouraging. Personally it makes me feel like the method is harder to understand that it actually is. I would prefer if it had been displayed that way:

Note that the success of a cast at runtime is modulo Scala’s erasure semantics. For example:

  • 1.asInstanceOf[String] throws a ClassCastException at run-time because the Int and String are distinct after erasure; but
  • List(1).asInstanceOf[List[String]] does not because List[Int] and List[String] are not distinct after erasure.

From there, if you’re not a C++ developer who got used to believe everything not explicitly documented has UB, it would be good to read something along those lines:

Do not rely on thrown exception to determine whether or not a conversion via asInstanceOf was successful. Behavior is undefined In cases where the language isn’t able to catch an invalid conversion because of erasure (as shown above).

And finally I would add some notes about ways to work around the erasure thing.

5 Likes

I can’t agree more. There are things that are nearly impossible to change and then there are the names of dangerous methods that require extra care in reviews and maintenance.

Further, changes of any kind are possible when there’s a smooth migration path. That’s obviously the case here. The compiler can emit a deprecation note that says "asInstanceOf is deprecated and will be soon replaced by “asUnsafeInstanceOf (or any color you like for the bike shed)”.

This kind of deprecation has precedent in other languages. I would not surprised if Scala veterans could name examples in Scala too.

That’s a very good argument.

If I can go off on a little tangent, I would add that I personally believe Scala lacks a lightweight way to safely narrow a type. Pattern matching certainly works, but even if I don’t go crazy with newlines that’s still 3 lines in braceless syntax.

val x = y match
  case z: T => Some(z)
  case _ => None

I very much agree. compile-time safety is many times better than its run-time counterpart.

Note that I would also emit a warning in the following situation:

val x = Int
val p = x.isInstanceOf[String]
// conversion from Int to String always fails

IMO there’s no point writing predicates that will always fail. So if the compiler is able to make this determination, it should issue a warning.

That’s an excellent point!

1 Like

I disagree.

One issue to consider is that “unsafe” means different things to different people. My personal definition, which comes from C++, is that unsafe means “prone to UB”. In my experience, that’s typically not what unsafe means for JVM enthusiasts, which is one of the main selling points of using a virtual machine in the first place!

Hence, I would argue that asInstanceOf is more unsafe than most “unsafe” (as described by most Scala developers) operations. Crucially, it is unsafe in a way that makes bugs harder to debug than most other bugs one can write in Scala.

To perhaps better illustrate, consider this example in Swift:

let x: Any = 42
let y = x as! String
let z = unsafeBitCast(x, to: String.self)

If you ask a Swift developer, they will say that the cast initializing y is safe whereas the one initializing z isn’t. That’s because the first has defined behavior. It will invariably trap at run-time. The second will go through and then perhaps summon nasal demons, which are orders of magnitude harder to debug.

2 Likes

The example you gave in Swift behaves the same in Scala too. Casting to a String traps on failure, bit-bashing using sun.misc.Unsafe does not.

There is definitely trickiness around whether things trap in the presence of erasure, but I’d argue that’s not a Scala-specific thing. I’ve certainly hit “cast succeeds, code blows up somewhere downstream” bugs in Java as well. And for the new gradual type systems on dynamic runtimes, such as Typescript or MyPy, cast-succeeds-code-blows-up-downstream is the norm, as all static types are erased on those platforms.

I don’t disagree that the behavior in the original post is confusing, and that Scala has its own trickiness due to “additional” erasure of type constructs that do not exist in Java. But fundamentally I feel like this behavior around casts is really standard. I’m not actually familiar with any platform where casts are guaranteed to fail loudly; maybe C# with its reified generics? I’m unfortunately not familiar enough with Swift to discuss it in detail

1 Like

Thinking more on this, I actually disagree in this case.
I would go even further, I think 1.asInstanceOf[String] should not throw an error at runtime !

The reason is, if we do some thing in some cases, it creates an expectation that it does it always, that the compiler has your back.
And in this case, it very much does not !

We can see this effect even in this thread:

I am not sure even having a message like
“This conversion will always fail, did you mean … IMPORTANT: Note that this error message is only present for some misuse of asInstanceOf, DO NOT rely on it”
will actually diminish that effect

Maybe we should add something like this to the standard library (name TBD):

extension (x: Any)
  def castOption[T]: Option[T] = x match
    case y: T => Some(y)
    case _ => None

(And maybe a version that is TypeTest friendly)

I think Options make a lot of sense here, as they are basically a boolean + a return value

1 Like

You can make the example demonstrating a case would not trap in Scala if you want.

let x: Any = [1]
let z = x as! [String] // will trap in Swift, won't throw in Scala

My larger point is that there is a difference between an “unsafe” operation that will consistently throw if its preconditions are violated (e.g., division by 0) and one that could cause UB (e.g., asInstanceOf).

Swift will fail loudly for any conversion that cannot be guaranteed correct at runtime. That’s because as! (and as?, which is the feature returning the optional that I wish Scala had) are safe operations.

I don’t know if such safety guarantees can be offered in Scala but that’s another discussion. The point I’m making is that AFAICT most operations in Scala are safe (under my definition of safety, i.e., they can’t cause UB) but asInstanceOf isn’t. To me, that justifies adding “unsafe” somewhere in its name.