Contracts with Multiple Type Params

So far we've only demonstrated very simple contracts with over a single generic type parameter - however, there is no hard restriction on the number of type parameters that a contract can reference. (Of course, typical software design principles still apply, a contract with many type params is probably going to be too complex to actually be used effectively in practice.)

Here's a contract defined over multiple type params:

Fig 1:


atom IndexOutOfBounds
contract RandomAccess<C, E> {
  function read(collection: C, index: int) -> oneof<E, std::Error<IndexOutOfBounds>>;
  function write(collection: C, index: int, elem: E) -> oneof<std::OK, std::Error<IndexOutOfBounds>>;
}

And an implementation of that contract:

Fig 2:


newtype Node<T> : mut struct {
  val: T,
  next: oneof<Node<T>, std::Nothing>
}
function findNode<T>(head: Node<T>, index: int) -> oneof<Node<T>, std::Error<IndexOutOfBounds>> {
  # ...
  if (index < 0) { return std::Error(IndexOutOfBounds); }
  repeat (index) {
    var next = unwrap(head).next;
    if (next instanceof Node<T>) {
      head = next;
    } else {
      return std::Error(IndexOutOfBounds);
    }
  }
  return head;
}

implement RandomAccess<Node<string>, string> {
  function read(head: Node<string>, index: int) -> oneof<string, std::Error<IndexOutOfBounds>> {
    var found ?= findNode(head, index);
    return unwrap(found).val;
  }
  function write(head: Node<string>, index: int, elem: string) -> oneof<std::OK, std::Error<IndexOutOfBounds>> {
    var found ?= findNode(head, index);
    unwrap(found).val = elem;
    return std::OK;
  }
}

Note: Learn more about the ?= operator used in the above example in the Error Handling section.

Calling a Contract Procedure Over Multiple Type Params

A contract procedure is always called in exactly the same way regardless of how many type parameters the contract was defined over.

Fig 3:


newtype Node<T> : mut struct {
  val: T,
  next: oneof<Node<T>, std::Nothing>
}
function findNode<T>(head: Node<T>, index: int) -> oneof<Node<T>, std::Error<IndexOutOfBounds>> {
  if (index < 0) { return std::Error(IndexOutOfBounds); }
  repeat (index) {
    var next = unwrap(head).next;
    if (next instanceof Node<T>) {
      head = next;
    } else {
      return std::Error(IndexOutOfBounds);
    }
  }
  return head;
}

implement RandomAccess<Node<string>, string> {
  function read(head: Node<string>, index: int) -> oneof<string, std::Error<IndexOutOfBounds>> {
    var found ?= findNode(head, index);
    return unwrap(found).val;
  }
  function write(head: Node<string>, index: int, elem: string) -> oneof<std::OK, std::Error<IndexOutOfBounds>> {
    var found ?= findNode(head, index);
    unwrap(found).val = elem;
    return std::OK;
  }
}

var myLinkedList = Node(mut {val = "head", next = Node(mut {val = "middle", next = Node(mut {val = "tail", next = std::Nothing})})});

for (i in [-1, 0, 1, 2, 3]) {
  var readRes =
    # Explicitly constrain the return type, so Claro knows which contract implementation to dispatch to.
    cast(
      oneof<string, std::Error<IndexOutOfBounds>>,
      RandomAccess::read(myLinkedList, i)
    );
  print("index {i}: {readRes}");
}

Output:

index -1: Error(IndexOutOfBounds)
index 0: head
index 1: middle
index 2: tail
index 3: Error(IndexOutOfBounds)

Limitation of the Above Contract Definition

Notice that in the prior example, the call to RandomAccess::read(...) is wrapped in an explicit static cast(...). If you read closely, you can see that this is because the arguments alone do not fully constrain the type that the call should return (it could be that you intend to dispatch to some other impl RandomAccess<Node<string>, Foo>). Read more about this situation in Required Type Annotations.

By allowing this sort of contract definition, Claro actually opens up a design space for contracts that can have multiple slight variations implemented, enabling callers can conveniently just get the return type that they need based on context.

However, you could argue that this particular contract definition does not benefit from that flexibility. This contract would arguably be more useful if RandomAccess::read(...) didn't have an ambiguous return type.

Learn how to address this issue using "Implied Types"