Types

Refingere is statically typed: every value has a type known when the program compiles, and a mismatch is a compile error rather than a surprise at run time. This page covers the builtin types, how types are combined into new ones, and how to define your own. The Types reference lists every builtin type with its methods, and Interfaces every builtin interface.

Writing types

A type is written after a : - on parameters, return values, fields, and on a let or const when the inferred one isn't what you want. With := the type comes from the value. Function parameters and fields always say their type; locals usually don't.

Numbers

Numeric types say exactly how big they are: U8 to U256, S8 to S256, F16, F32 and F64. Integer and Float are other names for S64 and F64, and what a literal becomes when nothing says otherwise. Nothing converts implicitly: as converts, and is checks a value's type. Numbers covers conversions, overflow, floats and the math types.

Other builtin types

Text is String, stored as UTF-8 - UTF16, UTF32 and ASCII exist for other encodings, Char is a single Unicode code point, and Bytes is raw data. Trilean is a three-valued logic, for when "unknown" is a real answer.

The collections are generic: Array, Set, Map, Stack, Queue, Deque, BiMap, Table for rows like an SQL table, and Network for graphs. Beyond those there are Range, Iterator, Duration, Instant, Regex and the pointer types - all on the Types page.

Types built from types

A generic type takes its arguments in angle brackets - usually types, sometimes values, like a vector's size. A few operators build a new type out of others:

  • T? is a T or undefined - a value that might be missing.
  • E ! T is an error E or a T - the result of something that can fail. Errors are values, and this is how a function's type says it can return one - see Errors.
  • (A, B) is a tuple, and A & B joins tuples, flattening them into one.
  • A | B is an untagged union - a value of either type - described below.

The type decides what can be done with the value: a User? can't be used as a User until the missing case is handled - here with ?., which skips the access on undefined, and ??, which gives a fallback. The same operators, and a few more, handle errors; the Operators reference has them all.

Structs

A struct is a group of named fields. A value is built by writing the type's name directly against braces, with the fields in order or by name - fields with a default can be left out. A default is worked out again for each value built, so field seen: Map<String, U64> := Map.of() gives every value its own empty map. A λ taking nothing, as the default of a field that isn't a function, is called each time - field made: Instant := λ() &clock.now() is the same as := &clock.now(), and a longer default can be a λ with a block. A bare name spelled exactly like a field is shorthand for passing it by name, so Book{title} means Book{title := title}. Fields are read with . and changed with set. Besides field, a struct's body can hold a const, one value shared by the type rather than stored in each instance. See Struct Definition.

There's no inheritance. Instead, && builds a struct out of others: it has every field of each, and gets each one's implement blocks and methods, written again for it. It isn't a kind of them, though - a Sign can't be passed where a Named is wanted. Behaviour is shared through interfaces instead, below. See Combined types.

Enums

An enum is a value that's one of several cases. A case can carry nothing, values in order, or named fields, and it's built by naming it through its enum: Shape.Circle(2.0). match tells the cases apart and takes their data out. See Enum Definition and Match.

|| combines enums the way && combines structs: the new enum has every case of each. Naming a struct there adds a case of the same name, carrying that struct's fields. An interface or method every part has, the enum has too, each call going to the part its case came from.

A keyed enum, enum(U8) or enum(String), gives each case a key of that type: for storing it, sending it or reading it back. A key is written after the case's name and data with ≔. Left out, it's one more than the case before's for an integer type (0 for the first), and the case's name for String; any other key type needs every key written. Keys have to be different, and fit their type.

  • value.key() - the case's key.
  • Type.fromKey(key) - the case with that key, or undefined. Only a case carrying nothing can be made from its key alone.
  • Type.from(struct) - the case whose fields are the struct's: the same names, of the same types, the fields of the structs it's combined from with && included. It takes any of the program's structs that fit one case; one that fits several is left out, with a warning.

Unions

A union is a value of any one of its member types, like A | B given a name. Unlike an enum, it has no cases of its own: its members are told apart by their type, so a match takes one apart with type patterns - case F64 as n. Unions fit data whose members are already different types, like JSON; when two possibilities share a type, or should carry a name, use an enum. See Union Definition.

Type names

type gives an existing type another name, which can take type parameters of its own. It's the same type under a clearer name, not a new one. See Type Definition.

Behaviour and generics

Types hold data; what they can do is described by interfaces, implemented separately from the type, and code that works for many types takes type parameters - Array<T>, function longest<T: Describable>(...). Both are covered in Interfaces and Generics.