Of course, it’s valid to decide that this is an acceptable price to pay. TypeScript does this (with no runtime errors since the types basically just disappear), and I guess Dart does it as well.
If the List type constructor was contravariant instead (i.e. List<int?> was a subtype of List<int>), then you would have analogous problem with the types swapped:
oops(List<int> xs) {
int x = xs[0];
// do something with x
}
main() {
List<int?> blah = [null];
oops(blah);
}
So it doesn’t seem that this dichotomy is relevant here.
An immutable list is covariant in its type parameter. A mutable list is invariant, neither covariant or contravariant. The proper solution is for List<A> and List<B> to be unrelated types in the sub typing relation.
It is relevant. The covariance/contravariance of the type depends on whether it’s in the “input” position or the “output” position. But generally speaking, List is neither contravariant nor covariant.
Kotlin gets this correct with its in and out qualifiers on generic parameters.
“Valid” is a measured tradeoff between convenience and potential for errors. Because, it can cause runtime errors.
function addString<a>(lst: (a | string)[]) {
lst.push("oh no")
}
let xs: number[] = [1, 2, 3]
addString(xs)
// straightforward function contract, returns a number
function sum(lst: number[]): number {
return lst.reduce((a, b) => a + b)
}
console.log(sum([2, "bar"])) // <- this produces a type error
console.log(sum(xs)) // <- this doesn't, but it doesn't return a number
// will this behave as it looks like it will?
for (let i = 0; i <= sum(xs); i++) {
console.log("hi")
}
The alternative is explicit variance specification, or seriously weakening subtyping relations.
I thought the article was pretty good, other than glossing over the covariance/contravariance as @quasi_qua_quasi pointed out.
These days, I pretty strongly favor the Option approach specifically because of the nesting as mentioned in the article. You don’t always need that, but when you do, it pretty much forces you to swim upstream of the language if it chose the nullable approach.
Also, Swift’s approach is kind of a hybrid. I think it’s technically more like an Option, but it has a ton of syntax sugar so that it feels exactly like the nullable type approach except when you want to do an explicit switch statement.
Nullable types exist in the static type system, but the runtime representation of values uses the underlying type. If you have a “nullable 3”, at runtime it’s just the number 3. If you have an absent value of some nullable type, at runtime you just have the solitary magic value null.
I don’t see how this is possible; you need some out-of-band value to represent null. Magic doesn’t exist, it’s just something operating by rules you can’t see.
TypeScript null values report their own type. A value has the same runtime type no matter where it’s used. (This is a bit trivialized by the fact that TypeScript vanishes into JavaScript before runtime.)
Swift:
1> var foo: Optional<Int>
foo: Int? = 0
2> foo = 3 // Shorthand for Optional<Int>.some(3)
3> print(Mirror(reflecting: foo))
Mirror for Optional<Int>
4> foo = nil // Shorthand for Optional<Int>.none
5> print(Mirror(reflecting: foo))
Mirror for Optional<Int>
6> print(nil)
error: repl.swift:6:7: error: 'nil' is not compatible with expected argument type 'Any'
Swift null values are a case of the generic type Optional. The runtime type of an Optional is the same, regardless of whether it’s the .some case or the .none case. Here it’s either .some and contains an int, or it’s .none and does not contain anything. That is to say, nil is just a keyword that refers to a case of a certain standard library type. You can’t even print it without a concrete Optional type, because it is not itself a value. The out of band value you’re referring to is the bit of the Optional that indicates which case it is.
An interesting effect here is that Optional<Int>.none and Optional<SomethingElse>.none are both usually expressed by nil and then inferred in context, but they are distinct values, can’t be assigned to a variable of the other type, and are unlikely to be the same allocated size. Compare with TypeScript, where you can take a number | null, compare to see if it is null, and assign the result to a receiver of type string | null. Swift has no such value that can be copied between distinct concrete types of Optional.
It’s worth pointing out that this is not always sound. For example, this results in a runtime type error:
Of course, it’s valid to decide that this is an acceptable price to pay. TypeScript does this (with no runtime errors since the types basically just disappear), and I guess Dart does it as well.
Isn’t this basically type covariance vs type contravariance? Some languages (notably Scala) allow to explicitly specify which is desirable.
If the
List
type constructor was contravariant instead (i.e.List<int?>
was a subtype ofList<int>
), then you would have analogous problem with the types swapped:So it doesn’t seem that this dichotomy is relevant here.
An immutable list is covariant in its type parameter. A mutable list is invariant, neither covariant or contravariant. The proper solution is for
List<A>
andList<B>
to be unrelated types in the sub typing relation.It is relevant. The covariance/contravariance of the type depends on whether it’s in the “input” position or the “output” position. But generally speaking, List is neither contravariant nor covariant.
Kotlin gets this correct with its
in
andout
qualifiers on generic parameters.“Valid” is a measured tradeoff between convenience and potential for errors. Because, it can cause runtime errors.
The alternative is explicit variance specification, or seriously weakening subtyping relations.
TL;DR: If your language has used
null
pervasively, nullable types look like an easier upgrade than option types.I’m in shock that they copied Java’s design mistake here. WHY?
I thought the article was pretty good, other than glossing over the covariance/contravariance as @quasi_qua_quasi pointed out.
These days, I pretty strongly favor the Option approach specifically because of the nesting as mentioned in the article. You don’t always need that, but when you do, it pretty much forces you to swim upstream of the language if it chose the nullable approach.
Also, Swift’s approach is kind of a hybrid. I think it’s technically more like an Option, but it has a ton of syntax sugar so that it feels exactly like the nullable type approach except when you want to do an explicit
switch
statement.Friendly reminder, you can reference other users here with an
@
-prefix.Thank you. I wasn’t aware.
I don’t see how this is possible; you need some out-of-band value to represent
null
. Magic doesn’t exist, it’s just something operating by rules you can’t see.Would this work fine if everything is boxed? Without knowing the runtime of Dart, I wouldn’t be surprised if that’s true.
…heck you’re right, that’s the obvious answer.
Here’s an example of how it works in two languages I use:
TypeScript:
TypeScript null values report their own type. A value has the same runtime type no matter where it’s used. (This is a bit trivialized by the fact that TypeScript vanishes into JavaScript before runtime.)
Swift:
Swift null values are a case of the generic type Optional. The runtime type of an Optional is the same, regardless of whether it’s the .some case or the .none case. Here it’s either .some and contains an int, or it’s .none and does not contain anything. That is to say,
nil
is just a keyword that refers to a case of a certain standard library type. You can’t even print it without a concrete Optional type, because it is not itself a value. The out of band value you’re referring to is the bit of the Optional that indicates which case it is.An interesting effect here is that
Optional<Int>.none
andOptional<SomethingElse>.none
are both usually expressed bynil
and then inferred in context, but they are distinct values, can’t be assigned to a variable of the other type, and are unlikely to be the same allocated size. Compare with TypeScript, where you can take anumber | null
, compare to see if it isnull
, and assign the result to a receiver of typestring | null
. Swift has no such value that can be copied between distinct concrete types of Optional.