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:

Fig 1:


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);

Output:

(1, 4)
Error(IndexTooHigh)

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.

?= 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:

Fig 2:


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;
}