You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: README.md
+1-136Lines changed: 1 addition & 136 deletions
Original file line number
Diff line number
Diff line change
@@ -7,139 +7,4 @@ _Rust-like safe error handling in Julia_
7
7
8
8
ErrorTypes is a simple implementation of Rust-like error handling in Julia. Its goal is to increase safety of Julia code internally in packages by providing easy-to-use, zero-cost handling for recoverable errors.
9
9
10
-
## Motivation
11
-
You're building an important package that includes some text processing. At some point, you need a function that gets the length of the first word (in bytes) of some text. So, you write up the following:
Nice - easy, fast, flexible, relatively generic. You also write a couple of tests to handle the edge cases:
19
-
20
-
```julia
21
-
@testfirst_word_bytes("Lorem ipsum") ==5
22
-
@testfirst_word_bytes(" dolor sit amet") ==5# leading whitespace
23
-
@testfirst_word_bytes("Rødgrød med fløde") ==9# Unicode
24
-
```
25
-
All tests pass, and you push to production. But oh no! Your code has a horrible bug that causes your production server to crash! See, you forgot an edge case:
26
-
27
-
```julia
28
-
julia>first_word_bytes("boo!")
29
-
ERROR: MethodError: no method matching -(::Nothing, ::Int64)
30
-
```
31
-
32
-
The infuriating part is that the Julia compiler is in on the plot against you: It *knew* that `findfirst` returned a `Union{Int, Nothing}`, not an `Int` as you assumed it did. It just decided to not share that information, leading you into a hidden trap.
33
-
34
-
With this package, that worry is no more. You can specify the return type of any function that can possibly fail, so that it becomes a `Result` (or an `Option`):
Now, if you forget that `safer_findfirst` can error, and mistakenly assume that it always return an `Int`, *none* of your tests will pass, and you will catch the bug immediately instead of in production.
48
-
49
-
Notably, in fully type-stable code, using `ErrorTypes` in this manner carries precisely zero run-time performance penalty.
50
-
51
-
## Usage
52
-
53
-
When making a function safer, you should __always__ explicitly type assert its return value as either `Result{O, E}` or `Option{T}`, with all parameters specified.
54
-
Functions that can error, but whose error state does not need to carry information may be marked with `Option{T}`. A successful result `x` of type `T` should be returned as `Thing(x)`, and an unsuccessful attempt should be returned as `none`. `none` is the singleton instance of `None{Nothing}`, and is used as a stand-in for all values of none-valued `Option`s.
Functions that can error, and whose error must carry some information should instead use `Result{O, E}`. The `Ok` state is of type `O`, and the `Err` state of type `E`. A successful return value should return `Ok(x)`, an unsuccessful should return `Err(x)`. By itself, `Ok(x)` and `Err(x)` will return a dummy value that is converted to the correct type due to the typeassert.
The "error value" of an `Option{T}` and a `Result{O, E}` is a `None{T}` and `Err{O, E}(::E)`, respectively. The "result value" is a `Thing{T}(::T)` and a `Ok{O, E}(::O)`.
76
-
77
-
__Both__
78
-
*`unwrap(x::Union{Option, Result}`: throws an error if `x` contains an error value. Else, return the result value.
79
-
*`expect(x::Union{Option, Result}, s::AbstractString)`: same as `unwrap`, but errors with the custom string `s`.
80
-
81
-
82
-
__Option__
83
-
*`is_none(x::Option)`: Returns whether `x` contains a `None`.
84
-
*`unwrap_none(x::Option)`: Errors if the `Option` contains a result value, else returns `nothing`
85
-
*`expect_none(x::Option, s::AbstractString)` same as `unwrap_none`, but errors with the custom string `s`.
86
-
87
-
__Result__
88
-
*`is_error(x::Result)`: Returns whether `x` contains an error value.
89
-
90
-
### @?
91
-
If you make an entire codebase of functions returning `Result`s and `Options`, it can get bothersome to constantly check if function calls contain error values and propagate those error values. To make this process easier, use the macro `@?`, which automatically propagates any error values. If this is applied to some expression `x` evaluating to a `Result` or `Option` containing an error value, this returns the error value. If it contains a result value, the macro is evaluated to the content of the result value.
92
-
93
-
For example, suppose you want to implement a safe version of the harmonic mean function, which in turn uses a safe version of `div`:
In this function, we constantly have to check whether `safe_div` returned the error value, and return that from the outer function in that case. That can be more concisely written as:
Note that this package does not protect YOUR code from using unsafe functions, it protects everyone else relying on your code. For example:
126
-
127
-
```julia
128
-
unsafe_func(x::Int) = x ==1?nothing: (x ==4?missing: x +1)
129
-
130
-
functionstill_unsafe(x::Int)::Option{Int}
131
-
y =unsafe_func(x^2)
132
-
y ===nothing? none :Thing(y)
133
-
end
134
-
```
135
-
136
-
Here, you've forgotten the egde case `unsafe_func(4)`, so `still_unsafe` will error in that case. To correctly "contain" the unsafety of `unsafe_func`, you need to cover all three return types: `Int`, `Nothing` and `Missing`:
ErrorTypes provide an _easy_ way to efficiently encode error states in return types. Its types are modelled after Rust's `Option` and `Result`.
4
+
5
+
__Example__
6
+
7
+
```
8
+
using ErrorTypes
9
+
10
+
function safe_maximum(x::AbstractArray)::Option{eltype(x)}
11
+
isempty(x) && return none # return an Option with a None inside
12
+
return Thing(maximum(x)) # return an Option with a Thing inside
13
+
end
14
+
15
+
function using_safe_maximum(x)
16
+
maybe_val = safe_maximum(x)
17
+
println("Maximum is $(unwrap_or(x, "[ERROR - EMPTY ARRAY]"))")
18
+
end
19
+
```
20
+
21
+
See also: Usage [TODO]
22
+
23
+
__Advantages__
24
+
25
+
* Zero-cost: In fully type-stable code, error handling using ErrorTypes is as fast as a manual check for the edge case, and significantly faster than exception handling.
26
+
* Increased safety: ErrorTypes is designed to not guess. By making edge cases explicit, your code will be more reliable for yourself and for others.
27
+
28
+
See also: Motivation [TODO]
29
+
30
+
__Comparisons to other packages__
31
+
32
+
The idea behind this package is well known and used in other languages, e.g. Rust and the functional languages (Clojure, Haskell etc). It is also implemented in the Julia packages `Expect` and `ResultTypes`, and to some extend `MLStyle`. Compared to these packages, ErrorTypes offer:
33
+
34
+
* Efficiency: All ErrorTypes methods and types should be maximally efficient. For example, an `Option{T}` is as lightweight as a `Union{T, Nothing}`, and manipulation of the `Option` is as efficient as a `===` check against `nothing`.
35
+
* Increased comfort. The main disadvantage of using these packages are the increased development cost. ErrorTypes provides more quality-of-life functionality for working with error types than `Expect` and `ResultTypes`, including the simple `Option` type.
36
+
* Flexibility. Whereas `Expect` and `ResultTypes` require your error state to be encoded in an `Exception` object, ErrorTypes allow you to store it how you want. Want to store it as a `Bool`? Sure, why not.
37
+
* Strictness: ErrorTypes tries hard to not _guess_ what you're doing. Error types with concrete values cannot be converted to another error type, and non-error types are never implicitly converted to error types. Generally, ErrorTypes tries to not allow room for error.
In short, ErrorTypes improves the _safety_ of your error-handling code by reducing the opportunities for _human error_.
3
+
4
+
Some people think the source of programming bug are programs who misbehave by not computing what they are supposed to. This is false. Bugs arise from _human_ failure. We humans fail to understand the programs we write. We forget the edge cases We create leaky abstractions. We don't document thoroughly.
5
+
6
+
If we want better programs, it is pointless to wait around for better humans to arrive. We can't excuse the existence of bugs with the existence human fallibility, because we will never get rid of that. Instead, we must design systems to contain and mitigate human error. Programming languages are such systems.
7
+
8
+
One source of such error is _fallible functions_. Some functions are natually fallible. Consider, for example, the `maximum` function from Julia's Base. This function will fail on empty collections. With an edge case like this, some human is bound to forget it at some point, and produce fragile software as a result. The behaviour of `maximum` is a bug waiting to happen.
9
+
10
+
However, because we *know* there is a potential bug hiding here, we have the ability to act. We can use our programming language to _force_ us to remember the edge case. We can, for example, encode the edge case into the type system such that any code that forgets the edge case simply won't compile.
11
+
12
+
## An illustrative example
13
+
14
+
Suppose you're building an important package that includes some text processing. At some point, you need a function that gets the length of the first word (in bytes) of some text. So, you write up the following:
Easy, fast, flexible, relatively generic. You also write a couple of tests to handle the edge cases:
22
+
23
+
```julia
24
+
@testfirst_word_bytes("Lorem ipsum") ==5
25
+
@testfirst_word_bytes(" dolor sit amet") ==5# leading whitespace
26
+
@testfirst_word_bytes("Rødgrød med fløde") ==9# Unicode
27
+
```
28
+
29
+
All tests pass, and you push to production. But alas! Your code has a horrible bug that causes your production server to crash! See, you forgot an edge case:
30
+
31
+
```julia
32
+
julia>first_word_bytes("boo!")
33
+
ERROR: MethodError: no method matching -(::Nothing, ::Int64)
34
+
```
35
+
36
+
The infuriating part is that the Julia compiler is in on the plot against you: It *knew* that `findfirst` returned a `Union{Int, Nothing}`, not an `Int` as you assumed it did. It just decided to not share that information, leading you into a hidden trap.
37
+
38
+
We can do better. With this package, you can specify the return type of any function that can possibly fail. Here, we will encode it as an `Option{Int}`:
Now, if you forget that `safer_findfirst` can error, and mistakenly assume that it always return an `Int`, your `first_word_bytes` will error in _all_ cases, because almost no operation are permitted on `Option` objects.
52
+
53
+
## Why would you NOT use ErrorTypes?
54
+
Using ErrorTypes, or packages like it, provides a small but constant _friction_ in your code. It will increase the burden of maintenance.
55
+
56
+
You will be constantly wrapping and unwrapping return values, and you now have to annotate function's return values. Refactoring code becomes a larger job, because these large, clunky type signatures all have to be changed. You will make mistakes with your return signatures and type conversions, which will slow down deveopment. Furthermore, you (intentionally) place retrictions on the input and output types of your functions, which, depending on the function's design, can limit its uses.
57
+
58
+
So, use of this package comes down to how much developing time you are willing to pay for building more reliable software. Sometimes, it's not worth it.
__Option{T}__ encodes either the presence of a `T` or the absence of one. You should use this type to encode either the successful creation of `T`, or a failure that does not need any information to explain itself. Like its sibling `Result`, `Option` is a sum type from the pacage SumTypes.
5
+
6
+
An `Option` contains two _variants_, `None` and `Thing`. Like other sum types, you should usually not construct an `Option` directly. Instead, the constructors `None` and `Thing` creates an `Option` wrapping them.
7
+
8
+
* You should construct an `Option{T}` wrapping a `Thing` with `Thing(::T)`.
9
+
* You _can_`None` objects directly e.g. `ErrorTypes.None{Int}()`, although `None` is not exported. Normally, however, the object `none` (singleton of `None{Nothing}`) is convertible to any `Option{T}`. See below in the section "basic usage".
10
+
11
+
__Result{O, E}__ has the two variants `Ok`, representing the successful creation of an `O`, or else an `E`, representing some object carrying information about the error. You should use `Result` instead of `Option` when the error needs to be explained.
12
+
13
+
* You _can_ construct `Result` objects by `Ok{O, E}(::O)` or `Err{O, E}(::Err)`. However, normally, you can instead use the simpler constructors `Ok(::O)` and `Err(::E)`. These constructors creates `ResultConstructor` objects, which can be converted to the correct `Result` types.
14
+
15
+
### Basic usage:
16
+
Always typeassert any function that returns an error type. The whole point of ErrorTypes is to encode error states in return types, and be specific about these error states. While ErrorTypes will _technically_ work fine without function annotation, I highly recommend annotating return types:
Also, by annotating return functions, you use the simpler constructors of error types:
29
+
* You can use `none`, which is automatically converted to `Option` containing a `None`.
30
+
* You can use `Err(x)` and `Ok(x)` to create `ResultConstructor`s, which are converted by the typeassert to the correct `Result`.
31
+
32
+
### Conversion
33
+
Apart from the two special cases above: The conversion of `none` (of type `None`) to `Option`, and `Err(x)` and `Ok(x)` (of type `ResultConstructor`) to `Result`, error types can only convert to each other in certain circumstances. This is intentional, because type conversions is a major source of mistakes.
34
+
35
+
* An object of type `Option` and `Result` can be converted to its own type.
36
+
* An `Option{T}` containing a `Thing` can be converted to an `Option{T2}`, if `T <: T2`. You intentionally cannot convert e.g. an `Option{Int}` to an `Option{UInt}`.
37
+
* An `Option` containing a `None` can be converted to any other `Option`. This is to allow easy "propagation" of error values (see the `@?` macro).
38
+
* A `Result{O, E}` containing an `Ok` can be converted to a `Result{O2, E2}` if `O <: O2`. Similarly, if it contains an `Err`, it can be converted if `E <: E2`.
39
+
40
+
### @?
41
+
If you make an entire codebase of functions returning `Result`s and `Options`, it can get bothersome to constantly check if function calls contain error values and propagate those error values. To make this process easier, use the macro `@?`, which automatically propagates any error values. If this is applied to some expression `x` evaluating to a `Result` or `Option` containing an error value, this returns the error value. If it contains a result value, the macro is evaluated to the content of the result value.
42
+
43
+
For example, suppose you want to implement a safe version of the harmonic mean function, which in turn uses a safe version of `div`:
In this function, we constantly have to check whether `safe_div` returned the error value, and return that from the outer function in that case. That can be more concisely written as:
0 commit comments