Implicit Casts for Verified Type Information

I totally agree with Chris but sometimes I wish I could write something like below without force unwrapping:

guard error == nil else {
  print(error!.localizedDescription)
  completion(.failure(error!))
  return
}

So do I. In your example, what confusion could it cause? There is absolutely no reason to recheck error and the code is very clear:

guard error == nil else {
  print(error.localizedDescription)
  completion(.failure(error!))
  return
}

This sort of thing is the common case.

If I remember correctly, one of Swift's principles is no surprise. I think this implicit cast is a surprise, and hurts local reasoning.

How is it surprising? Note that you can still treat obj as Any and go on about your day unaware of its presence. The type hasn't changed. It just inserts a cast so you can call String methods, which is safe because you just checked if it is a String.

func printCharCount(obj:Any) {
  if obj is String {
    print("Chars: \(obj.count)")
  }
}

Having taught Java and C# developers Swift, I've found that many things in Swift surprise them. This would not be high on the list. The first time you see it in Kotlin you think, "OK. I just checked if obj is a String so the compiler is treating it like a String." You might not be aware of the mechanism (implicit cast), but it is hardly shocking.

1 Like

For all intents and purposes, the type has changed, even if the change might be fairly harmless.

Swift doesn't "just insert" casts, so your suggestion is a pretty big ideological deal. That doesn't mean your idea is wrong, but it does explain the pushback.

I also notice that no one in this thread has mentioned that this is source breaking (if the syntax is required to be print(obj.count) inside the special context), or potentially confusing (if print(obj.count) and print(obj?.count) or print(obj!.count) are silently accepted as the same thing inside the special context).

Generally, I don't think the whole idea really rises to the importance of something that should be a language change. If it did, it also seems like it would be useful to consider the idea in relation to other dissatisfactions around if, such as the multiple other threads about simplifying the syntax of if case statements. Maybe there is a unified solution that makes such a change worthwhile.

5 Likes

When you write:

func countChars(_ obj:Any) { 
     if obj is String { 
         print((obj as! String).count) 
     } 
 } 
 countChars("Akemi")
 5

...have you changed the type of obj? No. The need for an obj as! String cast has been removed because it is provably not required. Should it produce a warning about not being required? Probably.

True. I'm not trying to underplay it, but it is a working feature in Kotlin, a language with a couple million users, and other participants in this thread have pointed out that similar features have been proposed for Kotlin. It's not out of left field.

They aren't treated as the same thing. They are treated as if a cast to String was explicitly applied. Errors, warnings, etc. would be the same.

I suppose we have different interpretations of "surprise". Mine is wherever a user goes "Where is this from?" or "Why is it doing this? I didn't tell it to do this.". As far as I know, there is nowhere in Swift currently where the compiler pulls a sneaky on the user, at least not where types are involved. This implicit cast is sneaky, because as "implicit" suggests, the (temporary) type casting is not always communicated to the user.

On the other hand, I understand the motivation behind this pitch: if foo is Bar, then it's safe to cast foo as! Bar. And likely the user wants to use foo as! Bar too. However, there is still the question of whether the user always wants such a cast. I feel there are some cases where protocols are involved where this implicit cast behaviour can cause a lot of troubles. I can't think of any so far, but I'll post here when one does surface up my mind.

Surprise can go either way, though. Learning Swift in the past few years I have often been ‘surprised’ that the compiler wasn’t ‘smart’ enough to let me do what I should (obviously, with trivial proof) be able to do. It comes up most often in a setting like if x != nil {...} (then why can’t I use x without force-unwrapping?) and the same considerations apply here.

There may be other reasons to be skeptical of the proposal but I don’t personally feel that the suggested behavior would be very surprising.

Edit: I understand now that if x != nil {...} is not generally good style; my point is that this was itself somewhat surprising as I learned the language. I quickly learned the if let x = x syntax, and I think people would learn if obj is String { obj.stringMethod() } quickly too.

1 Like

I think the best thing to do on this would be to implement a warning and fix-it to replace e.g.

if obj is String {
   let x = obj!.count
}

with

if let obj = obj as? String {
   let x = obj.count
}

Note that if obj is used mutably, the fix-it shouldn't apply:

if obj is String {
   obj!.append("x")
}

is not the same as:

if var obj = obj as? String {
   obj.append("x")
}
2 Likes

Because Optional<Something> is a different type from Something.

It's not about how quickly you can learn something. Everyone can quickly learn that {} + [] == 0 in JavaScript, but it doesn't make it a good design.

There is already a if let foo = foo as? Bar syntax that doesn't surprise anyone, if you are looking for parallels between type casting and optional-unwrapping.

optional unwrapping type casting
if foo != nil if foo is Bar
if let foo = foo if let foo = foo as? Bar

If x is of type Optional<X>, it remains optional. if x != nil {...} doesn't change x's type. All it does is checking if x != Optional<X>.none.

Thanks. I do understand this aspect of the language as it is. I just disagree that it would be surprising if the language worked differently and automatically allowed the use of an optional variable as if it were the non-optional wrapped value, in a location where (1) it is trivially verifiable that the optional is non-nil (such as the scope of if x != nil {} and (2) the non-optional wrapped type, but not the optional, is valid from a type-checking perspective. I do see that it would be a little extra ‘magical,’ let’s say, and might well be unwise, I’m really not sure. I guess I just don’t think surprise is a factor one way or the other.

1 Like

I think this is a neat idea, but has several problems:

  1. it'd introduce two ways of doing the same thing
  2. the new way only work for let constants and local variables that have not been captured by a closure, so in many cases it won't work
  3. especially when it comes to Optional, the superposition of the wrapped type can introduce subtle ambiguities. For instance, if you have an optional array, check for nil, and then do array.map(…) which map does it call (the one on Optional or the one on Array)?

That said, the current way to do it with if let is worse on a few aspects:

  1. You can't mutate the original variable since you've copied it to a new one
  2. The syntax is more verbose

Frankly, I'd prefer solutions for problems of if let rather than introducing something that operates completely differently.

1 Like

Given that Swift does not have any notion of threads, this transformation shouldn't apply to anything that can be accessed from multiple threads. So this feature only applies to local variables, struct members, and immutable constants. Not even computed properties is included. The applicable scenarios are quite limited.

That is fair. In terms of programmer experience design, the surprise, or lack of, is often influenced by whatever languages the user already knows.

Except all of type inferencing, which is everywhere :slight_smile:

I don't think it does. Remember, the original type is not actually being changed.

I disagree. I use it in Kotlin daily. One common case is function parameters. For example, when you are implementing an interpreter and frequently checking types, type narrowing is very useful in switch (and in Kotlin, when) statements / expressions.

You obviously have deep knowledge of Swift's internals, and seem to have worked with Kotlin. Have you never used this in Kotlin?

No, I have not used Kotlin. I only know of it here and there because it gets compared to Swift, a lot...

I only looked up how Kotlin narrows the type of a variable, and it seems to have only this smart casting, so I can see why it's almost necessary in Kotlin. Though it also make me quite skeptical of its usefulness in Swift.

It's still not disputing my reasoning that it shouldn't work with global variables, class members, captured values. It would lead to quite a confusing discrepancy if that's really what the design entails.

Does

switch expr {
case let number as Number:
  // use it
case let string as String:
  // use it
}

suffice for this use case?

There's a lot of Anys being tossed around in examples. Are you really using Any so much that you need this sugar?

As a non-Any example, consider this:

var s: String? = ...

if s is String {
    print(s.map { $0.count })
    print(s.map { $0.asciiValue })
}

What does this print? Recall that String and Optional both have map methods. The first print only type-checks if it uses Optional.map and the second only type-checks if it uses String.map. So these two statements require different treatment of s. The type-checker's burden is increased, because either it considers both possibilities simultaneously as it checks each statement, or it has to run twice for one of the statements. I don't need my programs to take even longer to type-check.

9 Likes

We definitely have different definitions for "surprise" then. To explain myself better, by "surprise", I mean a language-agnostic kind of surprise, an instance where a user's assumption is subverted unexpectedly. It has nothing to do with one's familiarity with any specific language.

I consider this implicit casting a surprise, because unless a type change is requested by and/or indicated to the user, the user's assumption is that the variable's type remains the same; and because unless a variable's type has changed, it has access to properties defined in its own type only. There are programming languages that violate this, and I think they are bad designs.

Type inferencing, in my experience, happens only where the type is already indicated by the user and where there is a strong preference in the standard library (currency types, I think?). There is nothing surprising about it.

let anInteger = 1 // inferred as Int, and remains Int

This is exactly half of why I think this behaviour is a surprise, and dangerous.

The variable the user sees is still of its original type, unchanged. However, sometimes the things it does is as if it were a different type. And this in my opinion is even more confusing and dangerous than it always doing things as if it were a different type.

1 Like