Types
Ard’s type system has two layers:
- Built-in types that correspond directly to Go types. They follow Ard naming conventions but keep familiar Go semantics.
- Ard-native types that Go does not have, like
Maybe,Result, and type unions. These carry Ard’s stricter semantics.
Built-in Types
Section titled “Built-in Types”These types map directly onto Go:
| Ard | Go | Notes |
|---|---|---|
Str | string | Immutable UTF-8 text |
Int | int | Platform-sized integer |
Float64 | float64 | Default floating-point type |
Bool | bool | |
Byte | byte | Unsigned 8-bit value |
Rune | rune | One Unicode scalar value |
[T] | []T | Growable list |
Slice<T> | []T | Fixed-length shared list view |
[T; N] | [N]T | Fixed-size array |
[K:V] | map[K]V | Map |
Chan<T> | chan T | Typed channel |
Any | any | Opaque boxed value |
Sized numeric types are also available when a specific width or signedness is needed: Int8, Int16, Int32, Int64, Uint, Uint8, Uint16, Uint32, Uint64, Uintptr, and Float32, each corresponding to the same-named Go type. Literals are range-checked at compile time:
let small: Int8 = 127let port: Uint16 = 8080let ratio: Float32 = 1.5For ordinary code, prefer the default Int and Float64; reach for sized types when interfacing with Go APIs or when the width matters.
Primitives
Section titled “Primitives”let text: Str = "Hello, World!"let number: Int = 42let decimal: Float64 = 3.14let flag: Bool = truelet byte: Byte = 255let rune: Rune = 'é'let newline: Rune = '\n'Byte represents an unsigned 8-bit value (0..255). Rune represents one Unicode scalar value. Single-quoted rune literals make scalar comparisons concise, such as ch == '/' while iterating a string. Rune literals support escapes like '\n', '\x00', and '\u0080'.
Convert text explicitly with:
let bytes: [Byte] = "text".bytes()let runes: [Rune] = "text".runes()let from_bytes: Str = Str::from(bytes)let from_runes: Str = Str::from(runes)Str::from([Byte]) mirrors Go’s string([]byte) conversion; validate bytes first if your program needs to reject invalid UTF-8.
Numeric conversions
Section titled “Numeric conversions”Numbers convert through a static function on the target type. Which one you use depends on whether the conversion can lose information, and the compiler enforces the choice:
let wide: Float64 = Float64::from(ratio) // losslesslet count: Int? = Int::try(big) // checked, none when it does not fitlet low: Byte = Byte::fit(hash) // forced, wrapsfrom only compiles when every source value is exactly representable in the
target. When it is not, the error names the alternative:
Int::from(big) // error: Int cannot hold every Int64 value // use `Int::try` for a checked conversion or `Int::fit`try returns a Maybe, so a conversion that might not fit is handled like any
other optional:
let ms = Int::try(elapsed).or(0)fit always succeeds: integers wrap, floats round, and a float converted to an
integer saturates at the target’s bounds with NaN becoming 0.
Int, Uint, and Uintptr are 32 or 64 bits depending on the platform, so a
conversion involving them is only lossless when it holds on every platform.
That is why Int::try(x: Int64) is checked even though it always succeeds on a
64-bit machine, and why converting an Int to a Float64 uses fit — an
Int can carry more precision than a Float64 holds.
A Rune is always a valid Unicode scalar value, so it has no fit. Only a
Byte widens into it losslessly; anything else uses Rune::try.
See Go interop for the full rules, including foreign named scalar types.
Strings support checked Go-style byte slicing with optional bounds:
"hello".slice() // Str? containing "hello""hello".slice(start: 1) // Str? containing "ello""hello".slice(end: 4) // Str? containing "hell""hello".slice(start: 1, end: 4) // Str? containing "ell"Bounds are UTF-8 byte offsets, matching Go string slicing and Str.size(). Invalid bounds return none. A byte range can split a multi-byte encoding and produce invalid UTF-8; use runes() when boundaries must follow Unicode scalar values. On the Go target, a small substring may retain the larger source string’s storage.
Collections
Section titled “Collections”// Listslet numbers: [Int] = [1, 2, 3, 4, 5]let names: [Str] = ["Alice", "Bob", "Charlie"]
// Fixed-size arrayslet rgb: [Byte; 3] = [255, 128, 0]let empty: [Int; 0] = []
// Mapslet scores: [Str:Int] = ["Alice": 95, "Bob": 87]let config: [Int:Str] = [0: "zero", 1: "one", 2: "two"]Lists and maps behave like Go slices and maps, with methods like .size(), .push(), and .at() in place of Go’s built-in functions. Slice<T> is a fixed-length shared view created by a list or another slice’s checked .slice() method. Use .to_list() when independent growable storage is required. Fixed-size arrays behave like Go arrays: the length is part of the type, so [Byte; 3] and [Byte; 4] are distinct types. Lists, slices, and arrays support .at(), which returns a Maybe instead of panicking or returning a zero value.
Any is an opaque boxed value, corresponding to Go’s any. Any Ard value can be assigned to it, but unlike Go there is no type assertion syntax: an Any cannot be inspected, called, or unboxed without an explicit API such as unsafe::cast.
let boxed: Any = 42Ard-Native Types
Section titled “Ard-Native Types”These types have no direct Go equivalent. They are where Ard’s opinionated semantics live.
Void represents non-existence. It is rarely written except in function signatures that explicitly return no value.
Nullable Types (Maybe)
Section titled “Nullable Types (Maybe)”Ard has no nil. A value that may be absent is declared with the ? suffix, or formally as Maybe<T>, and the compiler requires the absent case to be handled.
mut maybe_name: Str? = Maybe::new()maybe_name = Maybe::new("Alice")
let formal: Maybe<Int> = Maybe::new(42)When the wrapped type starts with mut, group it before adding ?:
struct Widget {}let maybe_ref: (mut Widget)? = Maybe::new()Working with Maybe values:
let maybe_name: Str? = Maybe::new("Alice")
// Pattern matchingmatch maybe_name { name => "Hello, {name}!" _ => "Hello, stranger!"}
// Checking presencelet has_value: Bool = maybe_name.is_some()let is_empty: Bool = maybe_name.is_none()
// Providing defaultslet name: Str = maybe_name.or("Anonymous")See Maybe for the full API.
Results
Section titled “Results”Fallible operations return a Result, written T!E. Where Go returns (T, error) and trusts the caller to check it, Ard makes the error part of the type and the compiler enforces handling.
fn divide(a: Int, b: Int) Int!Str { if b == 0 { Result::err("division by zero") } else { Result::ok(a / b) }}See Error Handling and ard/result for details.
Type Unions
Section titled “Type Unions”Type unions allow a value to be one of several types:
type Printable = Str | Inttype Value = Int | Float64 | Str
let item: Printable = "Hello"let data: Value = 42Use match expressions to handle each type in a union:
use go:fmt
type Content = Str | Int | Bool
fn describe(value: Content) Str { match value { Str => "Text: {it}" Int => "Number: {it}" Bool => "Flag: {it}" }}
let items: [Content] = ["hello", 42, true]for item in items { fmt::Println(describe(item))}The it variable is automatically bound to the matched value.
Type Inference
Section titled “Type Inference”The compiler infers types from context, so annotations are usually optional:
let count = 42let items = [1, 2, 3]let scores = ["Alice": 95, "Bob": 87]Generic Syntax
Section titled “Generic Syntax”Use a $ prefix on a type to indicate a generic (unspecified) type:
fn identity(value: $T) $T { value}See Generics for more.