Feature: Nullable and Non-Optional Nullable Types
Overview
Swift currently provides an optional type system to handle values that may be absent. However, there are cases where a non-optional value may still be null at runtime, leading to potential bugs and crashes. This feature introduces two new type annotations, Nullable and NonOptionalNullable, that provide a way to express nullability for non-optional values.
Goals
The goals of this feature are to:
- Provide a clear and explicit way to handle null values for non-optional types in Swift.
- Reduce the potential for null-related bugs and crashes in Swift code.
- Integrate with existing Swift type system and syntax in a natural and intuitive way.
Design
Type Annotations
The two new type annotations, Nullable and NonOptionalNullable, are parameterized types that can be used to annotate variables, properties, and function parameters and return types.
-
Nullable<T>: A type that can hold either a value of type T or a nil value.
-
NonOptionalNullable<T>: A type that can hold either a value of type T or a nil value, but not a default value of T.
For example, a variable of type Nullable<Int> can hold either an integer value or a nil value, while a variable of type NonOptionalNullable<String> can hold either a non-empty string or a nil value.
Type Inference and Overload Resolution
The Swift type checker will be modified to allow Nullable and NonOptionalNullable types to be used in place of their non-nullable counterparts wherever they are expected. Type inference and overload resolution will be updated to support these types.
-
T? will continue to represent an optional T.
-
T! will continue to represent a non-optional T.
-
Nullable<T> will be used to represent a non-optional value that can be null at runtime.
-
NonOptionalNullable<T> will be used to represent a non-optional value that can be null at runtime.
Syntax
The syntax for Nullable and NonOptionalNullable types will be similar to the existing syntax for optional types.
-
T? will continue to represent an optional T.
-
T! will continue to represent a non-optional T.
-
Nullable<T> will be written as T? or Nullable<T>.
-
NonOptionalNullable<T> will be written as T! or NonOptionalNullable<T>.
Runtime
The Swift runtime will be updated to support Nullable and NonOptionalNullable types.
- A separate bit will be allocated to track whether a value is
nil or not for each nullable and non-optional nullable type.
- Memory layout and alignment will be adjusted to accommodate the null tracking bit.
- Null-checking logic will be added to the generated code for any operations that may read or write nullable values.
- Conversion between nullable and non-nullable types will be supported.
Pseudocode:
func foo(x: Nullable<Int>) {
if let value = x {
print("x has value: \(value)")
} else {
print("x is nil")
}
}
let a: Nullable<Int> = 1
foo(x: a) // Prints "x has value: 1"
bbrk24
2
I don't understand the difference between nullable and optional. For most structs, this proposed Nullable type seems to behave identically to Optional, though Optional has a smaller memory footprint for classes, unsafe pointers, and most enums. (There also seems to be a special case that String and Optional<String> are the same size, and I don't fully understand why.)
I'm especially confused by NonOptionalNullable. What exactly does "but not a default value" mean?
7 Likes
Thank you for reading.
I apologize if my explanation was not clear enough. I will try to clarify my idea. I hope it will be helpful.
Currently, Swift's optional type system provides a way to represent values that may be absent, but it doesn't have a way to express the idea of a non-optional value that could still be null at runtime. This can lead to bugs where developers forget to check for null values.
That is why I humbly suggest a new type annotation that indicates that a non-optional value may be null at runtime.
Jon_Shier
(Jon Shier)
4
Unless you’re bridging from another language and the bridged API is incorrect (as we see in Apple’s frameworks occasionally), how can any Swift value that isn’t an optional be null?
10 Likes
AlexanderM
(Alexander Momchilov)
5
Yeah, values imported from C APIs or unsafe APIs are the only ways I can think of that this can happen
class C {}
let validObjectReference = C()
print(validObjectReference)
let nullReference = unsafeBitCast(nil as UnsafeRawPointer?, to: C.self)
print(nullReference) // Boom
Hence the unsafe in unsafeBitCast.
davdroman
(David Roman)
6
I believe you're misconstruing "null" to mean "empty". An empty String isn't considered null in Swift, it's just a string that happens to be empty. Similarly for Int, 0 is not considered null. In general, primitive values in Swift don't have "defaults" like other languages do.
If you're looking for a reliable way of representing non-empty strings and arrays at runtime, I suggest you check out this library: GitHub - pointfreeco/swift-nonempty: 🎁 A compile-time guarantee that a collection contains a value.
3 Likes
i do understand the idea here, and it is useful in serialization/encoding tasks to be able to elide values that are "empty" (but non-nil). to avoid confusion with Optional, i prefer to refer to this concept as Elidable.
i disagree with the proposed direction, to model this a generic Elidable<T> type. Elidable should be a protocol instead.
protocol Elidable
{
init()
var isEmpty:Bool { get }
}
i think this could be very useful, surely more useful than Identifiable. (how often do you declare that conformance but never really use it?)
1 Like
Pretty much every SwiftUI view relies heavily on Identifiable—that's why it was added, no?
It's often a good basis for Hashable as well.
1 Like
that’s fair, my experience is with more of the linux ecosystem, where there are fewer large frameworks that use Identifiable a lot.
Pampel
(Pampel)
10
I'm still not sure I get it, can you give an example?
When deserialising, for example, JSON, I've often wanted a way to distinguish between 'this field was explicitly NULL in the JSON' and 'this field was not present in the JSON' but I don't think this is the same thing, and could probably be handled with nested optionals.
tera
11
Nulls are not preserved with JSONEncoder / JSONDecoder, but you can use JSONSerialization:
let string = #"{"foo":null}"#
let obj = try! JSONSerialization.jsonObject(with: string.data(using: .ascii)!) as! [String: Any]
let data = try! JSONSerialization.data(withJSONObject: obj)
let string2 = String(data: data, encoding: .ascii)!
print(obj) // ["foo": <null>]
print(string2) // {"foo":null}
if let value = obj["foo"] {
if value is NSNull {
print("nil") // prints: nil
} else {
print("non nil: \(value)")
}
} else {
print("absent")
}
when encoding, if an array field is empty, you often want to elide the field entirely, rather than encode an empty array.
1 Like
itaiferber
(Itai Ferber)
19
That's correct. Static overloads can be used from any module where those overloads are visible — if they are exposed publicly from Module A, and modules B and C are compiled with those overloads present, they can be used.
However, the scenario I posed is far from hypothetical:
- Module A is the stdlib, and the type in question is an stdlib type
- Module B is a package you use, which uses the stdlib
- Module C is your app, which uses both the stdlib and that package
If you're not careful, one archive can contain multiple subtly- (or not so subtly-!) different representations of the same type.
1 Like