Skip to content
LogoLogo

Generics

Lithe's generics are specialized at compile time. Each instantiation compiles to code for its concrete types, with no runtime dispatch and no casts.

Generic functions

Type parameters come in parentheses before the value parameters:

storage is record {
  a : address;
}
 
pure function pick(T)(const first : T; const second : T; const useFirst : bool) : T is {
  var chosen : T := second;
  if useFirst then { chosen := first; }
} with chosen
 
external view function ownerOr(const other : address; const flag : bool) : address is
  pick(storage.a, other, flag)

Type arguments are inferred from the call. Every argument for T must have the same type:

type parameter `T` is both `address` and `uint`

Generic modules

A module can take type parameters. Its types and functions can use them, and a use such as Set(address) names one specialization:

import "./lib/set";
 
type OrderId is bytes32;
 
storage is record {
  admins : Set(address);
  filled : Set(OrderId);
}
 
external function addAdmin(const who : address) is {
  Set.add(storage.admins, who);
}
 
external view function isAdmin(const who : address) : bool is
  Set.contains(storage.admins, who)
 
external function fill(const id : bytes32) is {
  Set.add(storage.filled, OrderId(id));
}
  • Set(address) is a type. Here it is map(address, bool), so it can be stored.
  • Set.add(storage.admins, who) calls the specialization picked by the argument's type.
  • Set(OrderId) and Set(bytes32) are different types, because nominal types stay distinct in generics too.
  • A module's other types are reached through a specialization: SetEnumerable(address).iterator.
  • A generic module's types can use other generic modules at its own parameters. The standard library's MapEnumerable(K, V) keeps its keys in a SetEnumerable(K).

Constraints

A type parameter can carry a constraint that the compiler checks at each use. The only constraint today is MapKey, which accepts types that can key a map: integers, bool, address, bytesN, wei, timestamp, and nominal types over them.

module Set(T : MapKey) is {
  type Self is map(T, bool);
 
  function add(storage set : Self; const value : T) is {
    set[value] := true;
  }
}
 
storage is record {
  names : Set(string);
}
 
external function go() is { skip }
type `string` does not satisfy `MapKey`

Storage parameters

A module function that declares a storage parameter, such as storage set : Self, receives a reference to that storage location, not a copy, and may write it. A const parameter is always a value. Set.add(storage.admins, who) writes to storage.admins. That is how collection modules work without owning a named domain of their own.

A module passes its own storage the same way. Inside a module that declares storage roles, Set.add(roles.admins, who) writes to roles.admins.

The storage can be a map's entry, or anything past one: Checkpoints.push(votes.history[account], key, value) works on account's history. Each key is computed once, at the call.