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
}
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.
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.
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.
Swift doesn't "just insert" casts, so your suggestion is a pretty big ideological deal.
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.
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 (ifprint(obj.count)
andprint(obj?.count)
orprint(obj!.count)
are silently accepted as the same thing inside the special context).
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.
How is it surprising?
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.
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.
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")
}
It comes up most often in a setting like
if x != nil {...}
(then why can’t I usex
without force-unwrapping?) and the same considerations apply here.
Because Optional<Something>
is a different type from Something
.
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 theif let x = x
syntax, and I think people would learnif obj is String { obj.stringMethod() }
quickly too.
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 |
It comes up most often in a setting like
if x != nil {...}
(then why can’t I usex
without force-unwrapping?)
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.
I think this is a neat idea, but has several problems:
let
constants and local variables that have not been captured by a closure, so in many cases it won't workOptional
, 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:
Frankly, I'd prefer solutions for problems of if let
rather than introducing something that operates completely differently.
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.
I suppose we have different interpretations of "surprise".
That is fair. In terms of programmer experience design, the surprise, or lack of, is often influenced by whatever languages the user already knows.
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.
Except all of type inferencing, which is everywhere
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.
I don't think it does. Remember, the original type is not actually being changed.
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.
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.
One common case is function parameters.
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.
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.
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 Any
s 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.
In terms of programmer experience design, the surprise, or lack of, is often influenced by whatever languages the user already knows.
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.
Except all of type inferencing, which is everywhere
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
the original type is not actually being changed
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.