Skip to content

Commit

Permalink
Continue Updating Claro Docs to Use Auto-Validated Examples
Browse files Browse the repository at this point in the history
  • Loading branch information
JasonSteving99 committed Jan 23, 2024
1 parent 775cd88 commit fdd44ff
Show file tree
Hide file tree
Showing 11 changed files with 198 additions and 0 deletions.
2 changes: 2 additions & 0 deletions mdbook_docs/src/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ write_source_file(
"//mdbook_docs/src/copying/known_bugs:known_bugs",
"//mdbook_docs/src/copying/mutability_coercion:mutability_coercion",
"//mdbook_docs/src/copying/performance:performance",
"//mdbook_docs/src/error_handling:error_handling",
"//mdbook_docs/src/error_handling/error_propagation:error_propagation",
"//mdbook_docs/src/generics/contracts:contracts",
"//mdbook_docs/src/generics/contracts/implementing_contracts:implementing_contracts",
"//mdbook_docs/src/generics/generic_return_type_inference:generic_return_type_inference",
Expand Down
2 changes: 2 additions & 0 deletions mdbook_docs/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@
- [Mutability Coercion on Copy](./copying/mutability_coercion/mutability_coercion.generated_docs.md)
- [Performance Optimizations](./copying/performance/performance.generated_docs.md)
- [Known Copy Bugs](./copying/known_bugs/known_bugs.generated_docs.md)
- [Error Handling](./error_handling/error_handling.generated_docs.md)
- [Error Propagation](./error_handling/error_propagation/error_propagation.generated_docs.md)
- [Concurrency](./concurrency.md)
- [Graph Procedures](./graph_procedures/graph_procedures.generated_docs.md)
- [Graph Procedure Composition](./graph_procedures/graph_procedure_composition/graph_procedure_composition.generated_docs.md)
Expand Down
16 changes: 16 additions & 0 deletions mdbook_docs/src/error_handling/BUILD
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",
],
)
36 changes: 36 additions & 0 deletions mdbook_docs/src/error_handling/error_handling.tmpl.md
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.**

13 changes: 13 additions & 0 deletions mdbook_docs/src/error_handling/error_propagation/BUILD
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,
}
],
)
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 mdbook_docs/src/error_handling/error_propagation/ex1.claro
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 mdbook_docs/src/error_handling/error_propagation/ex2.claro
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;
}
1 change: 1 addition & 0 deletions mdbook_docs/src/error_handling/ex1.claro
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
newtype Error<T> : T
20 changes: 20 additions & 0 deletions mdbook_docs/src/error_handling/ex2.claro
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;
$$}
34 changes: 34 additions & 0 deletions mdbook_docs/src/error_handling/ex3.claro
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;
$$}

0 comments on commit fdd44ff

Please sign in to comment.