Generic Return Type Inference

One very interesting capability that you get from the combination of Claro's bidirectional type inference and generics is the ability to infer which Contract implementation to defer to based on the expected/requested return type at a procedure call-site. Let's get more specific.

Fig 1:


contract Index<T, R> {
    function get(l: T, ind: int) -> R;
}

implement Index<[int], int> {
    function get(l: [int], ind: int) -> int {
        return l[ind];
    }
}

atom IndexOutOfBounds
newtype SafeRes<T> : oneof<T, std::Error<IndexOutOfBounds>>

implement Index<[int], SafeRes<int>> {
    function get(l: [int], ind: int) -> SafeRes<int> {
        if (ind >= 0 and ind < len(l)) {
            return SafeRes(l[ind]);
        }
        return SafeRes(std::Error(IndexOutOfBounds));
    }
}

For the above implementations of Index<T, R>, you'll notice that each function, Index::get, only differs in its return type but not in the arg types. So, Claro must determine which implementation to defer to by way of the contextually expected return type. This, I believe leads to some very convenient ergonomics for configurability, though the onus for "appropriate" use of this feature is a design decision given to developers.

Fig 2:


var l = [1,2,3];

var unsafeRes: int = Index::get(l, 1);
var safeRes: SafeRes<int> = Index::get(l, 1);

print(unsafeRes);
print(safeRes);

safeRes = Index::get(l, 10);
print(safeRes);

# Out of bounds runtime err.
# unsafeRes: int = Index::get(l, 10);

Output:

2
SafeRes(2)
SafeRes(Error(IndexOutOfBounds))

Ambiguous Calls

As described in further detail in the section on Required Type Annotations, certain generic procedures that return a value of a generic type may require the call to be explicitly constrained by context. In particular, this will be the case when the generic type does not appear in any of the procedure's declared arguments.

For example, calling the above Index::get Contract Procedure will statically require the "requested" return type to be statically constrained by context:

Fig 3:


# Ambiguous Contract Procedure Call - should the call return `int` or `SafeRes`?
var ambiguous = Index::get(l, 10);

Compilation Errors:

generic_return_type_inference_EX3_example.claro:2: Ambiguous Contract Procedure Call: Calls to the procedure `Index$<T, R>::get` is ambiguous without an explicit type annotation to constrain the expected generic return type `R`.
var ambiguous = Index::get(l, 10);
                ^^^^^^^^^^^^^^^^^
1 Error

Ambiguity via Indirect Calls to Contracts

Note that while this specific ambiguity can only possibly arise as a result of calls to a Contract procedure, even indirect calls can cause this problem:

Fig 4:


requires(Index<C, R>)
function pickRandom<C, R>(collection: C, maxInd: int) -> R {
  return Index::get(collection, random::nextNonNegativeBoundedInt(random::create(), maxInd + 1));
}

# Ambiguous Contract Procedure Call - should the call return `int` or `SafeRes`?
var ambiguous = pickRandom([1, 2, 3], 10);

Compilation Errors:

generic_return_type_inference_EX4_example.claro:7: Invalid Generic Procedure Call: For the call to the following generic procedure `pickRandom` with the following signature:
		`function<|[int], int| -> R> Generic Over {[int], R} Requiring Impls for Contracts {Index$<[int], R>}`
	The output types cannot be fully inferred by the argument types alone. The output type must be contextually constrained by either a type annotation or a static cast.
var ambiguous = pickRandom([1, 2, 3], 10);
                ^^^^^^^^^^^^^^^^^^^^^^^^^
1 Error

Again, you can resolve this issue by explicitly declaring the "requested" return type:

Fig 5:


requires(Index<C, R>)
function pickRandom<C, R>(collection: C, maxInd: int) -> R {
  return Index::get(collection, random::nextNonNegativeBoundedInt(random::create(), maxInd + 1));
}

# Ambiguous Contract Procedure Call - should the call return `int` or `SafeRes`?
var unambiguous: SafeRes<int> = pickRandom([1, 2, 3], 10);
print(unambiguous);

Output:

SafeRes(Error(IndexOutOfBounds))