7 ms·
The reason why languages promote variable types based on control flow is because developers en masse actually expect that to happen, e.g. facing the code like
by mraleph 3y ago
The reason why languages promote variable types based on control flow is because developers en masse actually expect that to happen, e.g. facing the code like
Dog? maybeDog;
if (maybeDog != null) {
maybeDog.bark();
}
If compiler says "sorry, maybeDog might be null", developer (rightfully so) usually responds "but I have just checked and I know it is not, why do you bother me?". So languages chose to accommodate this.
> What if I want to set the value back to nil if it is not-nil?
You can. The type of the variable does not actually change. You could say that the information about more precise type is simply propagated to the uses which are guarded by the control flow. The following will compile just fine:
Dog? maybeDog;
if (maybeDog != null) {
maybeDog.bark();
maybeDog = null;
}
> Why should I have to wrap things back in an optional if I want to pass it along as such?
You don't, with a few exceptions. There is a subtype relationship between T and T?: T <: T?, so the following is just fine:
void foo(Dog? maybeDog);
Dog? maybeDog;
if (maybeDog != null) {
maybeDog.bark();
foo(maybeDog); // Dog can be used where Dog? is expected
}
You might need to account for it in places where type inference infers type which is too precise, e.g.
Dog? maybeDog;
if (maybeDog != null) {
// This will be List<Dog> rather than List<Dog?>.
final listOfDogs = [maybeDog];
}
Though I don't think it is that bad of a problem in practice.
- saagarjha 3y agoSee, I don't think languages should accommodate this, because I see it as an ugly solution. It's nice that it works in a few cases but then it very quickly breaks down: a developer finds that their null check is enough to strip an optional, but a test against zero doesn't convert their signed integer to unsigned. Checking that a collection has elements in it doesn't magically turn it into a "NonEmptyCollection" with guaranteed first and last elements. I'm all for compilers getting smarter to help do what programmers expect, but when they can't do a very good job I don't find the tradeoff to be very appealing. Basically, I think pattern matching is a much better solution this problem, rather than repurposing syntax that technically means something else (even though 90% of the time people who reach for it mean to do the other thing). Also, fwiw, I was mostly talking about things like the last example you gave. I guess it would be possible that in circumstances where T is invalid but T? would be valid, the language actually silently undos the refinement to make that code work. However, I am not sure this is actually a positive, and it doesn't help with the ambiguous cases anyways.
- refulgentis 3y agoThis is the elegant solution. Where does it break down? This isn't the same thing as "magically" changing the type. What does the syntax technically mean? What refinement is undone? I think you're a bit too wedded to the idea that there's a type conversion going on or some dark magic or something behind the scenes that's "done" then "undone" like a mechanism. There isn't. It's just static analysis. Same as: let x: Animal; x = Dog() if (x is Dog) { x.bark() } The Zen koan you want to ponder on your end is, why do you want to A) eliminate polymorphism from OOP B) remove the safety of a compiler error if the code is changed to x = Cat()?
- saagarjha 3y agoI don’t like that either. x is Dog is a boolean expression. Presumably I can write let isDog = x is Dog; And the value whether it is a dog or not goes into that new variable. The fact that I can’t then immediately go if (isDog) { x.bark() } shows the deficiencies of this static analysis (even if you could do some simple value tracking to make this work, it doesn’t really address the real issue I have with it, which I described above: why can’t I do this refinement for other things?) The conceptual model is one of “we special cased 2-3 cases that we can detect”, which I don’t find very elegant, especially considering that other languages seem to have better solutions that express the operation in what I feel is a better way. (The equivalent Swift code would be something like this: let x = Animal() if let dog = x as? Dog { dog.bark() } I see this as much superior. One, because x is still available in the scope of I want an Animal and not a Dog for whatever reason. I understand that most of the time you don’t do this but to have it available and not have to do a special case for it is nice. The second thing is that I just like the syntax, since it gives you an opportunity to give a more specific name in that branch as part of the assignment. Third, it composes, because none of the operations are special save for the “if let” binding. This code is also valid: let x = Animal() let dog /* : Dog? */ = x as? Dog if let dog /* = dog */ { } The static analysis for undoing polymorphism is the exact same as the one for binding to optionals, because the idiomatic way to cast results in an optional.)
- 3y ago