Type functions

Hash supports functions both at the value level and at the type level. Type-level functions correspond to generics in other languages. They are declared using angular brackets (< and >) rather than parentheses, and all parameters are types. Other than that, they have the same syntax as normal (value-level) functions.

Type-level functions can be used to create generic structs, enums, functions, and traits. For example, the generic Result<T, E> type would is defined as

Result := <T, E> => enum(
  Ok(T),
  Err(E),
);

This declares that Result is a function of kind <T: type, E: type> -> type. The default bound on each type parameter is type, but it can be any trait (or traits) as well. Multiple trait bounds can be specified using the ~ binary operator. For example,

Result := <T: Clone ~ Eq, E: Error ~ Print> => enum(
  Ok(T),
  Err(E),
);

Here, T must implement Clone and Eq, and E must implement Error and Print.

In order to evaluate type functions, type arguments can be specified in angle brackets:

my_result: Result<i32, str> = Ok(3);

When calling functions or instantiating enums/structs, type arguments can be inferred so that you don't have to specify them manually:

RefCounted := <Inner: Sized> => struct(
  ptr: &raw Inner,
  references: usize
);

make_ref_counted := <Inner: Sized> => (value: Inner) -> RefCounted<Inner> => {
  data_ptr := allocate_bytes_for<Inner>();

  RefCounted( // Type argument `Inner` inferred
    ptr = data_ptr,
    references = 1,
  )
};

my_ref_counted_string = make_ref_counted("Bilbo bing bong"); // `Inner = str` inferred

In order to explicitly infer specific arguments, you can use the _ sigil:

Convert := <I, O> => trait {
  convert: (input: I) -> O;
};

// ...implementations of convert

x := 3.convert<_, str>(); // `I = i32` inferred, `O = str` given.
x := 3.convert<I = _, O = str>(); // same thing.
x: str = 3.convert(); // same thing.

Type functions can only return types or functions; they cannot return values (though this is planned eventually). This means that you cannot write

land_with := <T> => land_on_moon_with<T>();
signal := land<Rover>;

but you can write

land_with := <T> => () => land_on_moon_with<T>();
signal := land<Rover>();

Just like with value-level functions, type-level functions can be provided with named arguments rather than positional arguments. These are subject to the same rules as value-level functions:

make_repository := <
  Create, Read,
  Update, Delete
> => () -> Repository<Create, Read, Update, Delete> => {
  ...
};

repo := make_repository<
  Create = DogCreate,
  Read = DogRead,
  Update = DogUpdate,
  Delete = DogDelete
>();

Finally, type-level function parameters can be given default arguments, which will be used if the arguments cannot be inferred from context and aren't specified explicitly:

Vec := <T, Allocator: Alloc = GlobalAllocator> => struct(
  data: RawRefInAllocator<T, Allocator>,
  length: usize,
);

make_vec := <T> => () -> Vec<T> => { ... }; // `Allocator = GlobalAllocator` inferred
make_vec_with_alloc := <T, Allocator: Alloc> => (allocator: Allocator) -> Vec<T, Allocator> => { ... };

x := make_vec<str>(); // `Allocator = GlobalAllocator` inferred
y := make_vec_with_alloc<str, _>(slab_allocator); // `Allocator = SlabAllocator` inferred

Grammar

The grammar for type function definitions and type function types can be found in the Types section.