As I'm writing more Rust code these days, it is fascinating to see how having or not having a standardized feature or concept in version 1.0 of the language can fundamentally change the language's surrounding ecosystem.
While it is easy to point to Rust's borrow checker and the ownership and lifetime rules of the language, there are more generally understood features and concepts which could apply to most existing programming languages.
Take the Result
type.
Result
Conceptually, Result
encapsulates return values from a function. Result
is usually implemented as an enum
with Ok/.success and Err/.failure variants. Both variants have associated values; for example, a
Result<Person, IOError>
could associate a Person
value in the success case or an IOError
value in the
failure case. Functions can return a Result
, and the function caller can exhaustively check the returned
Result
's value and handle the success and failure scenarios.
Here are very contrived examples:
In Rust:
struct CustomError {
message: String,
}
fn custom_sqrt(value: f64) -> Result<f64, CustomError> {
if value < 0.0 {
Err(CustomError { message: String::from("the value was negative.") })
} else {
Ok(value.sqrt())
}
}
fn caller() {
let x: f64 = 2.0;
let computed_result = custom_sqrt(x);
match computed_result {
Ok(computed_value) => println!("Hooray. Got value: {}", computed_value),
Err(error) => println!("Uh-oh, something went wrong: {}", error.message),
}
}
In Swift:
struct CustomError: Error {
var message: String
}
func customSqrt(_ value: Double) -> Result<Double, CustomError> {
guard value >= 0.0 else {
return .failure(CustomError(message: "The value was negative"))
}
return .success(sqrt(value))
}
func caller() {
let x: Double = 2.0
let computedResult = customSqrt(x)
switch computedResult {
case let .success(computed_value):
print("Hooray. Got value: \(computed_value)")
case let .failure(error):
print("Uh-oh, something went wrong: \(error)")
}
}
Result is basically implemented like:
In Rust:
pub enum Result<T, E> {
Ok(T),
Err(E),
}
In Swift:
public enum Result<S, F: Error> {
case success(S),
case failure(F)
}
Note that in both languages, Result
is implemented like any other enum and does not require
any special compiler support. 4 lines of code to define the essential functionality of the type.
In the end, Result
is a basic type.
Result
is in Rust 1.0's standard library, and the type has had a profound impact on Rust's ecosystem.
In contrast, Result
was added to Swift 5.0's standard library, and the type now has to content with other ways
of handling errors.
Rust
In Rust code, Result
is one of the most common types used. It is used in Rust's standard library and
many third party libraries. It is such a common type that the ?
operator was added in Rust
1.13 to make Result
(and Option
) easier to use. In practically all Rust
code, if there is a function that can either return a successful value or an error, the function has
a Result
return type.
Swift
On the other hand, in Swift code, there are multiple ways defined to return success and failure results from functions:
Returning a normal value but throwing an error
func couldThrowSomething() -> Int throws {
if Bool.random() {
return 1
} else {
throw CustomError(message: "Uh-oh")
}
}
func caller() {
do {
let value = try couldThrowSomething()
} catch {
// Magical "error" variable
print("Error was \(error)")
}
}
Returning a tuple with optional success and error values:
func compute() -> (Int?, Error?) {
if Bool.random() {
return (1, nil)
} else {
return (nil, CustomError(message: "Uh-oh"))
}
}
func caller() {
let result = couldThrowSomething()
switch result {
case let (.some(value), nil):
print("Success. Got: \(value)")
case let (nil, .some(error)):
print("Error. Got: \(error)")
case (nil, nil), (.some(_), .some(_)):
fatalError("There should be either a success or a failure value and also not both")
}
}
While less commonly used, the style of returning optional success and error values is seen in asynchronous callback APIs.
An example (which predates Swift but shows how multiple optional return values can be an issue) is in the infamous completion block of URLSession. Unless a developer carefully reads and understands the documentation, it is not trivial to understand exactly when a value will be present.
Returning a custom Result
type
Several developers independently created their own Result
implementations.
However, only APIs under their control would return their Result
type, and there is a potential of a conflict
if their projects imported dependencies and each had their own Result
types. Often this would require a mapping
function which mapped custom Result
to another custom Result
.
Swift developers have embraced all 3 styles of returning values from functions. Now a Swift developer has to contend with things like:
-
Which style of error handling to use?
-
Figuring out the error style of each API called.
-
Mapping the error style of APIs to the preferred error style.
The post-1.0 addition of Result
to Swift may cause some issues, but at least Swift has an Optional
type built
into the language from the initial version.
Optional
Java has lacked an Optional
type until Java 8 (
which was released 18 years after Java 1.0). Any Java developer is certainly familiar with
NullPointerException
s. While Swift's late adoption of Result
may lead to some issues, null references
have been called the billion dollar mistake (if not many times more).
Optional
is not a silver bullet towards solving all of the issues caused by null references, but
it probably could have saved at least a few hundred million dollars.
Unfortunately, the lack of a feature or concept in 1.0 of a language may have an irreversible impact to the
language's ecosystem. Never mind the herculean task of creating a Java standard library that has the Optional
concept as part of its core design, there is too much existing code that would also need to be
converted, so NullPointerException
s will likely always remain part of a Java developer's life.
Of course, lacking features in 1.0 of a language does not mean the language is doomed. Java has enabled corporations to make billions of dollars. Java has been adopted by many developers and is actively used by more developers than Swift and Rust developers combined today. Developers and users have extracted enormous value from using Java. If anything, Java proves that minimum viable products do exist for programming languages.
Still, 4 lines of code made many years ago does have an impact especially on my current day to day programming.
Rust would not be the same without the Result
type. While Rust and Swift are actually more alike than any of the
other modern mainstream programming languages, there are seemingly small fundamental 1.0 details which have caused
outsized divergences between the languages and their ecosystems.