Pre-SIP: deprecate asInstanceOf, introduce unsafeAsInstanceOf

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.

created an issue for intellij:
https://youtrack.jetbrains.com/issue/SCL-22167/documentation-missing-for-methods-defined-in-Any

and one for metals:

4 Likes

Who cares about what Java does?

Scala is not Java, and a Scala dev should not be required to know anything about Java!!! Thatā€™s a longstanding misunderstanding in the Scala community: Scala is not just some syntax sugar atop Java. Such a point of view is actively harmful to Scala as a language. (This issue is also something that shows in some other corners like the poor Scala documentation and tutorials which almost always assume that youā€™re an expert on all Java idiosyncrasies. This needs to stop finally!)

Java is a different language. Itā€™s ā€œunsafeā€ all the way down, itā€™s casting and NPE hell, and actually the best you could do in case you wanted to arrive at some sane design is to do the exact opposite of almost everything Java does.

Same goes for any other dynamic language. (Java is at its core a dynamic language, just rewatch the advertisements from the 90ā€™s in case you missed that fact; being a dynamic language is core to the problem with Java!). So regarding Python or TS:

Some tacked on unsound pseudo type-system doesnā€™t change anything. These languages are unsafe and unsound at their core. Imho the result is even bigger trash as not having a static type system in the first place, as in such case you at least not lulled into believing that the ā€œtypesā€ could safe your ass.

So both examples are not the yardstick for anything here.

Where?

Maybe in the mindset of the 70ā€™s of last centuryā€¦

Repeating all the C nonsense over and over, even 60 years later, is just bananas imho.

The new ā€œstandardā€ are safe conversions with ā€œasā€. Itā€™s the year 2024.

Itā€™s actually a kind of joke that of all things I need to point to MS and something out of Apple, two companies that arenā€™t know for their high technical standards, more the contrary, but both do "the right thing"ā„¢ nowadays.

E.g. C#:

using System.Collections.Generic;					

public class Program
{
	public static void Main()
	{
		int[] ints = [10, 20, 30];
		IEnumerable<int> numbers = ints;
		
		var TheAnser = (int)"42";
		var CastedNumbers = (IEnumerable<string>)ints;
	}
}

This code will fail at compile time with:

Compilation error (line 10, col 18): Cannot convert type 'string' to 'int'
Compilation error (line 11, col 23): Cannot convert type 'int[]' to 'System.Collections.Generic.IEnumerable<string>'

And that arenā€™t even the ā€œsafe conversionsā€! These are castsā€¦

So it shouldnā€™t compile? (Like in C#?)

Because not throwing an error here at runtime is not an option. This needs to fail.

The only alternative were nasal daemonsā€¦ Nobody wants that.

Thatā€™s nothing about Scala. Thatā€™s runtime behavior of the VM. And actually the JVM does here the right thing (as the VM is the only mostly sane part of Javaā€¦). Just doing nothing and than exploding with completely random behavior at the other end of the world, like JS does, is imho much worse. Thatā€™s exactly one of the main reasons why JS is dreaded by so many people: If you donā€™t work with the highest caution things in JS just blow up at ā€œrandomā€ places.

The issue here is that asInstanceOf is the ā€œunsafeā€ ā€œhey compiler, look the other wayā€ operator. Itā€™s not as. Itā€™s the ā€œbitcastā€.

And such an operation is needed.

Only that it shouldnā€™t be completely stupid imho. It should bail out on obviously bad cases. (See C#). But it still needs to work for the cases where the compiler canā€™t determine whether something is ā€œright or wrongā€ (thatā€™s where the C# thingy gets annoying because it doesnā€™t do that, itā€™s too strict, so you need to do nonsense like ā€œdouble castingā€ going through object, if you really know better but the compiler canā€™t understand that). Otherwise we wouldnā€™t have an ā€œescape hatchā€ for the cases where our type system would reject valid and useful programs. (Every static type system that guaranties that it never accepts invalid, unsound programs will experience cases where it rejects valid programs. Thatā€™s a hard rule, afaik there is a formal prove for it, so ā€œescape hatchesā€ are strictly needed. But they shouldnā€™t be maximally ā€œblindā€, and there should be some ā€œsafe variantsā€ also, imo).

Iā€™m not sure you understood me:
(The code = 1.asInstanceOf[String])

  1. The code currently compiles
  2. I believe it should keep compiling (i.e. not emit an error)
  3. I believe it might be dangerous for it to emit a warning
    1. As this might make it seem safer than it is
    2. ā€œThe compiler warned me last time, surely it will warn me again if I mess upā€
    3. This might be the case even if the warning is explicit about not being reliable
  4. I believe it might be better if it actually did not return a class cast exception, and instead randomly failed somewhere random
    1. I know this is probably impossible
    2. Users might get burned harder, but this would give the operator a very real sense of danger, and that there is no safeguard

And overall, I feel the most constructive given the current situation is to make the documentation around asInstanceOf better
Only then, if it is still a problem, should we think of renaming it

But that would be the same kind of warning you get with pattern matching (or ==).

It does not tell you that something is safe, but it tells you in case it would fail for sure.

Or would you also remove the other warnings and errors? Should "23" == 23 compile?

Burn down your house to teach the children not to play with matches?

Iā€™m not sure this is a good ideaā€¦ :slightly_smiling_face:

Thanks God at least on the JVM you will get a helpful, fast failing exception when itā€™s messed up obviously. Thatā€™s build in into the VM so Scala canā€™t change that. :pray:

I fully agree.

Itā€™s not like asInstanceOf would be a big problem in Scala. You usually donā€™t use it. (So you usually also donā€™t see it much in other peopleā€™s code). So itā€™s not the biggest issue. There are more important things. Better docu is always good, but else? I would like to have warnings for obviously bad cases, if itā€™s not to difficult. But I donā€™t think this has high prio on the wish list. :smile: