Skip to content

Types & Values

Almide is statically typed with full type inference. Every value has a known type at compile time. There are no implicit conversions, no null, and no truthiness.

TypeDescriptionExamples
Int64-bit signed integer42, 0xFF, 1_000_000
Float64-bit floating point3.14, 1.0e-3
StringUTF-8 text"hello", 'raw', """heredoc"""
BoolBooleantrue, false
UnitNo meaningful value()
PathFile system pathUsed by fs module functions
BytesByte sequenceUsed by crypto, fs for binary data

Numeric literals support _ as a visual separator: 1_000_000, 0xFF_FF.

Double-quoted strings support interpolation and escape sequences:

let name = "world"
let msg = "hello ${name}, 1+1=${1 + 1}" // "hello world, 1+1=2"
let escaped = "line1\nline2"

Single-quoted strings support escapes (\', \\, \n, \t, \r) but no interpolation:

let s = 'it\'s a string'

Raw strings have no escapes and no interpolation:

let pattern = r"^\d+\.\d+$"

Heredoc strings strip leading whitespace based on minimum indent:

let sql = """
SELECT *
FROM users
WHERE active = true
"""

Raw heredocs (r"""...""") disable both escapes and interpolation.

Boolean values are true and false. Almide uses keyword operators for logic:

let a = true and false // false
let b = true or false // true
let c = not true // false

There is no && or ||. Use and, or, and not for boolean logic. (The postfix ! is the unwrap operator, not boolean negation.)

Unit represents “no meaningful value.” Its only value is (). Functions that perform side effects and return nothing use Unit:

effect fn log(msg: String) -> Result[Unit, String] = {
println(msg)
ok(())
}

Lists are ordered, homogeneous collections:

let xs = [1, 2, 3] // List[Int]
let empty: List[String] = [] // empty list
let first = xs[0] // index access: 1

Mutable lists support index writes:

var items = ["a", "b", "c"]
items[1] = "B" // ["a", "B", "c"]
list.push(items, "d") // ["a", "B", "c", "d"]

+ concatenates lists:

let combined = [1, 2] + [3, 4] // [1, 2, 3, 4]

See the list stdlib for full API.

Maps are key-value collections. Map literals use square brackets with : separating keys and values:

let m = ["name": "Alice", "role": "admin"] // Map[String, String]
let empty: Map[String, Int] = [:] // empty map

Map access returns Option[V]:

let name = m["name"] // some("Alice")
let age = m["age"] // none

Mutable maps support writes:

var scores: Map[String, Int] = [:]
scores["alice"] = 100
scores["bob"] = 85

See the map stdlib for full API.

Sets are unordered collections of unique values:

let s = set.from_list([1, 2, 3, 2]) // Set containing 1, 2, 3
set.contains(s, 2) // true

Tuples are fixed-size collections of heterogeneous types:

let pair = (1, "hello") // (Int, String)
let triple = (true, 42, "ok") // (Bool, Int, String)

Access tuple elements with .0, .1, etc.:

let x = pair.0 // 1
let y = pair.1 // "hello"

Tuples are commonly used for multiple return values and for loop destructuring:

for (i, item) in list.enumerate(items) {
println("${int.to_string(i)}: ${item}")
}

Option[T] represents a value that may or may not exist. It replaces null:

let found = some(42) // Option[Int] with a value
let missing = none // Option[Int] with no value

Use match to handle both cases:

match list.get(xs, 0) {
some(x) => println("found: ${int.to_string(x)}"),
none => println("empty list"),
}

See Error Handling for more on Option.

Result[T, E] represents a computation that can succeed or fail:

let success = ok(42) // Result[Int, String]
let failure = err("not found") // Result[Int, String]

Use match to handle both cases:

match int.parse(input) {
ok(n) => println("parsed: ${int.to_string(n)}"),
err(e) => println("error: ${e}"),
}

See Error Handling for effect fn, unwrap operators (!, ??, ?), and guard.

Records are named product types with labeled fields:

type User = {
name: String,
age: Int,
active: Bool,
}

Create records with the same syntax:

let alice = { name: "Alice", age: 30, active: true }

Access fields with .:

println(alice.name) // "Alice"

Update records with spread:

let inactive = { ...alice, active: false }

Field shorthand works when the variable name matches the field name:

let name = "Bob"
let age = 25
let bob = { name, age, active: true }

Variants (algebraic data types) represent values that can be one of several cases:

type Shape =
| Circle(Float)
| Rect{ width: Float, height: Float }
| Point

Three case forms are supported:

FormSyntaxExample
Unit| CaseName| Point
Tuple| CaseName(Types...)| Circle(Float)
Record| CaseName{ fields... }| Rect{ width: Float, height: Float }

Use match to destructure variants:

fn area(s: Shape) -> Float =
match s {
Circle(r) => 3.14159 * r ^ 2,
Rect{ width, height } => width * height,
Point => 0.0,
}

Inline variants (without leading |) work for simple enumerations:

type Direction = North | South | East | West

Types can be parameterized with type variables using []:

type Pair[A, B] = {
first: A,
second: B,
}
let p: Pair[Int, String] = { first: 1, second: "one" }

See Generics for details.

Type aliases create transparent alternative names for existing types:

type Score = Int
type Handler = (String) -> String

Aliases are interchangeable with their underlying type.

Function types describe callable values:

type Predicate = Fn(Int) -> Bool
type Transform = Fn(String) -> String

Used with higher-order functions:

fn apply(f: Fn(Int) -> Int, x: Int) -> Int = f(x)

Types can derive built-in conventions using : after the type name:

type Color: Eq =
| Red
| Green
| Blue

This generates implementations automatically. Available conventions: Eq, Repr, Ord, Hash, Codec. See Protocols for details.

Eq and Hash are automatically derived by the compiler for all value types (except function types). No explicit deriving is needed:

let same = color_a == color_b // just works

See Protocols for user-defined protocols.