Skip to content

Types and simple generics

Kiroha checks types before running a program. Type inference is local: variable bindings use their initializer, lambdas use an expected function type or parameter annotations, and a generic call matches its arguments. Named function parameters are annotated. The checker does not infer a whole program's signatures or automatically generalize unannotated functions.

any is explicit. Missing information produces a type error asking for an annotation; it never silently becomes any.

Declare a structural type

type Point = {x:num, y:num};
p:Point = {x:1, y:2};
p.x + p.y
// 3

type declares a type name. Point = {x:num, y:num} is still an ordinary value binding containing schema values; it does not declare a type usable in annotations. Type declarations need a semicolon before another sequence item. They may appear in the same sequence positions as bindings and are lexical, with no forward references or recursive aliases.

A type is identified by its structure, not its alias name. A function requiring {x:num, y:num} also accepts a record with those fields and additional fields. Immutable array elements are covariant. Function parameters are contravariant and results covariant. An optional field may be absent; it is not automatically nullable.

The type forms are:

Form Meaning
str, num, int, bool, null, json String, number, integer-valued number, bool, null, arbitrary JSON
bytes, query, database, transaction, reply Opaque runtime kinds
[T] Immutable array with elements of type T
{x:T, y:U} Structural record
maybe(T) T or null
{x:optional(T)} A field that may be absent
open({x:T}) A record with additional JSON fields
(T, U)=>R A function with two ordinary arguments and no callback slot
(T){(U)=>V}=>R A function with an ordinary argument and a separate required callback
schema<T> A runtime validation descriptor whose successful result has type T
any Explicit dynamic escape hatch
never No normal return, for example a function that always fails

int is a subtype of num. Heterogeneous arrays and conditional branches can produce an internal union in an inferred type. Write json for a general JSON annotation and maybe(T) for nullability; there is no general union syntax.

Functions and lambdas

fun greet(name:str):str
  "Hello, " + name + "!"
end fun greet

greet("Ada")
// "Hello, Ada!"

Name each ordinary parameter's type. A non-recursive function can omit its result annotation and use the type of its final expression. A recursive function needs an explicit result type, so checking its body never requires a fixed-point inference pass.

fun factorial(n:int):int
  if n <= 1 then 1 else n * factorial(n - 1)
end fun factorial

factorial(5)
// 120

A lambda can obtain its parameter types from context:

increment:(num)=>num = {n => n + 1};
[1, 2, 3].map{n => increment(n)}
// [2,3,4]

Without that context, annotate the lambda's parameters: {n:num => n+1} or {(a:num, b:num) => a+b}. Its result is inferred locally. A standalone {x=>x} has no parameter type and asks for an annotation; it does not create an implicit polymorphic or dynamic function.

A named callback slot is annotated separately:

fun twice<T>(value:T){step:(T)=>T}:T
  step(step(value))
end fun twice

twice(2){n => n * 3}
// 18

The trailing callback is still a literal lambda and remains separate from ordinary arguments. For a callback type alias, give the named function an explicit result type, as in fun apply(x:num){step:Mapper}:num .... This distinguishes the header from a first-body record expression.

Generic types and functions

type Box<T> = {value:T};

fun box<T>(value:T):Box<T>
  {value:value}
end fun box

box(3).value
// 3

A generic declaration introduces type parameters, not runtime variables. Aliases can use several parameters and other aliases, for example type Pair<A,B> = {left:A, right:B}. Type argument counts are checked. There are no bounds, higher-kinded parameters or recursive type aliases.

Call arguments can determine a generic function's type arguments. Supply identity<num>(1) when an explicit choice is useful. The same syntax works for method type arguments, for example xs.fold<num>(0){(a,x)=>a+x}. If a type argument cannot be determined locally, write it explicitly.

Generic function bodies are checked for every permitted instantiation. With an unconstrained T, returning value:T is valid; doing value + 1 is not. Type parameters are erased at runtime. To validate generic data, pass a schema<T> value rather than trying to use T as an expression:

type Point = {x:num, y:num};
fun validate<T>(value:json, description:schema<T>):T
  value.as(description)
end fun validate

validate({x:1, y:2}, Point)
// {"x":1,"y":2}

Validate data at the boundary

get and parse return json, which is distinct from any. Validate unknown JSON before using a particular record or array shape:

type Point = {x:num, y:num};
point = parse("{\"x\":1,\"y\":2}").as[Point];
point.x + point.y
// 3

.as[Point] validates the value at runtime and gives the result type Point. For a schema value computed at runtime, including a schema<T> parameter, use value.as(description). Named concrete types can also be descriptors for input, including instantiated aliases such as Box<num>. Function types, schema<T> types and erased type parameters are annotation types; they cannot themselves be reified as validation descriptors.

Static structural compatibility permits extra record fields. Explicit runtime validation with v.as[Point] keeps the closed-record contract and rejects extras; use v.as[open(Point)] when extras should be accepted. Runtime validation also enforces data-dependent rules beyond static types.

Kiroha 0.7 used the earlier check function; see the 0.8 migration guide. A record field named as remains available as r.as, and indexed access can be written r["as"][key] when a type-shaped index would otherwise be parsed as validation. Use r["as"](arg) to call a function stored in that field, since r.as(arg) is validation syntax.

Use dynamic typing explicitly

value:any = 1;
try value + "x" catch {error => error.code}
// "type"

any allows assignments, calls and operations on that value to pass static checking. Runtime checks still apply. It propagates through code that uses it, including a lambda called by an explicitly dynamic callee. It does not turn off checking of unrelated expressions: value:any = 1 + "x" still has an invalid initializer. Use .as[...] rather than any when you want to establish a data contract.

Type errors use the check phase and cannot be caught by try. Useful codes include type, type_name, type_arity, type_annotation_required, duplicate_type_parameter, and type_reification. Runtime errors reached through explicit any, domain failures, permissions and I/O retain the existing runtime error/abort behavior.