-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Continue Updating Claro Docs to Use Auto-Validated Examples
- Loading branch information
1 parent
775cd88
commit fdd44ff
Showing
11 changed files
with
198 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
load("//mdbook_docs:docs_with_validated_examples.bzl", "doc_with_validated_examples") | ||
|
||
doc_with_validated_examples( | ||
name = "error_handling", | ||
doc_template = "error_handling.tmpl.md", | ||
examples = [ | ||
{ | ||
"example": "ex1.claro", | ||
"executable": False, | ||
"append_output": False, | ||
"codeblock_css_class": "claro", | ||
}, | ||
"ex2.claro", | ||
"ex3.claro", | ||
], | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
# Error Handling | ||
|
||
Claro takes a very principled stance that all control flow in the language should be modeled in a way that is | ||
self-consistent within the type system - as such, Claro chooses not to model errors around "throwing Exceptions". While | ||
many languages (e.g. Java/Python/C++/etc.) were designed around thrown exceptions as their error modeling tool, they all | ||
suffer from the same antipattern that make it impossible to determine strictly from looking at a procedure signature | ||
whether it's possible for the call to fail, and if so, what that failure might look like. This leads users into | ||
unnecessary digging to read implementation details to determine how and why certain unexpected error cases inevitably | ||
arise. | ||
|
||
So, taking inspiration from many prior languages such as Rust, Haskell, and Go, Claro requires errors to be modeled | ||
explicitly in procedures' signatures as possible return types so that all callers must necessarily either handle any | ||
potential errors, or explicitly ignore them or propagate them up the call stack. | ||
|
||
## `std::Error<T>` | ||
|
||
Claro's | ||
<a href="https://github.com/JasonSteving99/claro-lang/blob/main/stdlib/std.claro_module_api" target="_blank">std</a> | ||
module exports the following type definition: | ||
|
||
{{EX1}} | ||
|
||
This type is a trivial wrapper around any arbitrary type. Its power is in the special treatment that the compiler gives | ||
to this type to power Claro's error handling functionality. But first, let's take a look at how a procedure might make | ||
use of this type to represent states in practice - the below example demonstrates a function that models safe indexing | ||
into a list: | ||
|
||
{{EX2}} | ||
|
||
To drive the example home, instead of wrapping an atom which doesn't provide any information beyond the description of | ||
the error itself, the error could wrap a type that contains more information: | ||
|
||
{{EX3}} | ||
|
||
**Continue on to the next section to learn about how Claro enables simple propagation of `std::Error<T>` values.** | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
load("//mdbook_docs:docs_with_validated_examples.bzl", "doc_with_validated_examples") | ||
|
||
doc_with_validated_examples( | ||
name = "error_propagation", | ||
doc_template = "error_propagation.tmpl.md", | ||
examples = [ | ||
"ex1.claro", | ||
{ | ||
"example": "ex2.claro", | ||
"append_output": False, | ||
} | ||
], | ||
) |
28 changes: 28 additions & 0 deletions
28
mdbook_docs/src/error_handling/error_propagation/error_propagation.tmpl.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
# Error Propagation via `?=` Operator | ||
|
||
As mentioned in the previous section, the power of Claro's builtin `std::Error<T>` type comes from the special treatment | ||
that the compiler gives to values of that type. Specifically, Claro gives you the ability to early-return an error value | ||
from a procedure. If for some reason a procedure has no way of actually handling a potential error itself, it can opt to | ||
delegate the handling of the error to any callers. This allows the procedure doing error propagation to be written to | ||
handle only the "happy path". | ||
|
||
This example demonstrates a procedure that propagates potential errors to its callers: | ||
|
||
{{EX1}} | ||
|
||
<div class="warning"> | ||
|
||
**Note**: The error propagation above doesn't allow the caller to know details about whether the error came from the | ||
first or second call to `safeGet()`. This may or may not be desirable - but the design space is left open to Claro users | ||
to decide how they want to signal errors to best model the noteworthy states of their problem domain. | ||
</div> | ||
|
||
## `?=` Operator Drops All Error Cases | ||
|
||
You can observe in the above example that the `?=` operator will propagate any `std::Error<T>` found on the | ||
right-hand-side of the assignment. So, as a result, the value that reaches the variable on the left-hand-side of the | ||
assignment will drop all `std::Error<T>` variants from the `oneof<...>`. | ||
|
||
Below, some examples are listed to indicate the resulting type of the `?=` operator: | ||
|
||
{{EX2}} |
28 changes: 28 additions & 0 deletions
28
mdbook_docs/src/error_handling/error_propagation/ex1.claro
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
atom IndexTooHigh | ||
atom IndexTooLow | ||
function safeGet<T>(l: [T], i: int) | ||
-> oneof<T, std::Error<IndexTooHigh>, std::Error<IndexTooLow>> { | ||
# ... | ||
$$ if (i < 0) { | ||
$$ return std::Error(IndexTooLow); | ||
$$ } else if (i >= len(l)) { | ||
$$ return std::Error(IndexTooHigh); | ||
$$ } | ||
$$ return l[i]; | ||
} | ||
|
||
function getRandomPairFromList<T>(l: [T], rng: random::RandomNumberGenerator) | ||
-> oneof<tuple<T, T>, std::Error<IndexTooHigh>, std::Error<IndexTooLow>> { | ||
# std::Error may propagate from either call to safeGet(...). | ||
var first: T ?= safeGet(l, random::nextNonNegativeBoundedInt(rng, 5)); | ||
# Note the type annotation isn't necessary. | ||
var second ?= safeGet(l, random::nextNonNegativeBoundedInt(rng, 5)); | ||
return (first, second); | ||
} | ||
|
||
var rng = random::forSeed(0); | ||
|
||
var firstPair = getRandomPairFromList([1, 2, 3, 4], rng); | ||
print(firstPair); | ||
var secondPair = getRandomPairFromList([1, 2, 3, 4], rng); | ||
print(secondPair); |
18 changes: 18 additions & 0 deletions
18
mdbook_docs/src/error_handling/error_propagation/ex2.claro
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
$$atom A | ||
$$atom B | ||
$$atom C | ||
$$ | ||
provider demoErrorPropagation() -> oneof<A, B, std::Error<B>, std::Error<C>> { | ||
# When there would be multiple non-error variants, the result type remains a oneof<...>. | ||
var firstPotentialErr: oneof<A, B, std::Error<C>> = # ... | ||
$$ A; | ||
var firstTypeDemo: oneof<A, B> ?= firstPotentialErr; | ||
$$ _ = firstTypeDemo; | ||
|
||
# When there would only be a single non-error variant, the result type is narrowed to a concrete type. | ||
var secondPotentialErr: oneof<A, std::Error<B>, std::Error<C>> = # ... | ||
$$ A; | ||
var secondTypeDemo: A ?= secondPotentialErr; | ||
|
||
return secondTypeDemo; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
newtype Error<T> : T |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
atom IndexOutOfBounds | ||
function safeGet<T>(l: [T], i: int) -> oneof<T, std::Error<IndexOutOfBounds>> { | ||
if (i < 0 or i >= len(l)) { | ||
return std::Error(IndexOutOfBounds); | ||
} | ||
return l[i]; | ||
} | ||
|
||
var l = [1, 2, 3]; | ||
match (safeGet(l, getRandomIndex())) { | ||
case _:std::Error<IndexOutOfBounds> -> print("Index out of bounds!"); | ||
case X -> print("Successfully retrieved: {X}"); | ||
} | ||
# ... | ||
$$provider getRandomIndex() -> int { | ||
$$ random::forSeed(1) | ||
$$ |> random::nextNonNegativeBoundedInt(^, 8) | ||
$$ |> var i = ^; | ||
$$ return i; | ||
$$} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
atom TooHigh | ||
atom TooLow | ||
newtype IndexOutOfBounds : struct { | ||
reason: oneof<TooHigh, TooLow>, | ||
index: int | ||
} | ||
function safeGet<T>(l: [T], i: int) -> oneof<T, std::Error<IndexOutOfBounds>> { | ||
if (i < 0) { | ||
return std::Error(IndexOutOfBounds({reason = TooLow, index = i})); | ||
} else if (i >= len(l)) { | ||
return std::Error(IndexOutOfBounds({reason = TooHigh, index = i})); | ||
} | ||
return l[i]; | ||
} | ||
|
||
var l = [1, 2, 3]; | ||
match (safeGet(l, getRandomIndex())) { | ||
case std::Error(ERR) -> | ||
var unwrappedErr = unwrap(ERR); | ||
match (unwrappedErr.reason) { | ||
case _:TooHigh -> | ||
print("Index {unwrappedErr.index} is too high!"); | ||
case _:TooLow -> | ||
print("Index {unwrappedErr.index} is too low!"); | ||
} | ||
case X -> print("Successfully retrieved: {X}"); | ||
} | ||
# ... | ||
$$provider getRandomIndex() -> int { | ||
$$ random::forSeed(1) | ||
$$ |> random::nextNonNegativeBoundedInt(^, 8) | ||
$$ |> var i = ^; | ||
$$ return i; | ||
$$} |