"Narrowing" / Type Guards

Inspired by TypeScript's support for this, when you perform an instanceof check on a variable with a oneof type within a conditional statement, Claro automatically "narrows" the type of the variable to the checked type. This is logically valid because the only way that control-flow could possibly reach that context is if that was actually the type at runtime.

Fig 1:


var intOrStr: oneof<int, string> = 10;

if (intOrStr instanceof int) {
  # Claro automatically "narrows" the variable to have type `int`.
  var addRes = intOrStr + 10;
  print("{intOrStr} + 10 = {addRes}");
}

Output:

[0.003s][warning][perf,memops] Cannot use file /tmp/hsperfdata_runner/6 because it is locked by another process (errno = 11)
10 + 10 = 20

Note: Claro is not implementing full "flow typing" here. The type will be "widened" again to its originally declared type if you assign a value of any type other than the narrowed type to a variable in a context where it's been narrowed.

Fig 2:


var intOrStr: oneof<int, string> = 10;

if (intOrStr instanceof int) {
  # Claro automatically "narrows" the variable to have type `int`.
  var addRes = intOrStr + 10;
  print("{intOrStr} + 10 = {addRes}");

  # Claro automatically "widens" the variable to `oneof<int, string>`.
  intOrStr = "ten";
  addRes = intOrStr + 10;  # <-- This is no longer valid.
}

Compilation Errors:

narrowing_EX2_example.claro:10: Invalid type: found <oneof<int, string>>, but expected one of (<int, long, float, double>).
  addRes = intOrStr + 10;  # <-- This is no longer valid.
           ^^^^^^^^
1 Error

Non-Trivial Example Usage

For a less trivial example of working with oneof types, the below function is able to pretty-print a linked list by checking if the current node is the end of the list or not by branching on the type of the next reference:

Fig 3:


newtype LinkedNode<T> : struct {
  val: T,
  next: oneof<LinkedNode<T>, std::Nothing>
}

alias SB : string_builder::StringBuilder
function renderLinkedList<T>(head: LinkedNode<T>, sb: SB) -> SB {
  _ = string_builder::add(sb, "{unwrap(head).val} -> ");
  var next = unwrap(head).next;
  if (next instanceof LinkedNode<T>) {
    return renderLinkedList(next, sb);   # <-- Type of `next` was "narrowed" to `LinkedNode<T>`.
  } else {
    return string_builder::add(sb, "*END*");
  }
}

var linkedList = LinkedNode({val = 1, next = LinkedNode({val = 2, next = LinkedNode({val = 3, next = std::Nothing})})});

string_builder::create()
  |> renderLinkedList(linkedList, ^)
  |> string_builder::build(^)
  |> print(^);

Output:

1 -> 2 -> 3 -> *END*

The above example relies on concepts described in later sections, so consider checking out User Defined Types and Generics for some more info.