Skip to content

Generics

Almide uses [] for generics, not <>. This eliminates ambiguity with comparison operators and makes parsing unambiguous for both compilers and LLMs.

Declare type parameters in [] after the function name:

fn identity[T](x: T) -> T = x
fn first[T](xs: List[T]) -> Option[T] = list.first(xs)
fn pair[A, B](a: A, b: B) -> (A, B) = (a, b)

Type arguments are inferred at the call site:

let n = identity(42) // T = Int
let s = identity("hello") // T = String
let p = pair(1, "one") // A = Int, B = String

Types can be parameterized:

type Pair[A, B] = {
first: A,
second: B,
}
type Tree[T] =
| Leaf(T)
| Node(Tree[T], Tree[T])

Use with concrete types:

let p: Pair[Int, String] = { first: 1, second: "one" }
let tree: Tree[Int] = Node(Leaf(1), Leaf(2))

The standard library uses generics extensively:

TypeDescription
List[T]Ordered homogeneous collection
Map[K, V]Key-value collection
Set[T]Unordered unique collection
Option[T]Optional value (some(x) or none)
Result[T, E]Success or failure (ok(x) or err(e))

Nested generics:

let data: Map[String, List[Int]] = ["scores": [90, 85, 92]]
let result: Result[List[String], String] = ok(["a", "b"])

Type parameters can be constrained with protocol bounds using ::

fn show[T: Repr](item: T) -> String = item.repr()
fn process[T: Serializable](item: T) -> String = item.serialize()

Multiple bounds are joined with +:

fn show_named[T: Showable + Nameable](item: T) -> String =
item.name() + ": " + item.show()

A comma inside the brackets separates type parameters, not bounds — [T: A, B] declares a second type parameter named B.

See Protocols for defining custom protocols that can be used as bounds.

Almide always uses [] for type arguments:

List[Int] // list of integers
Map[String, List[Int]] // nested generics
Result[User, ParseError] // result type
Fn(Int, Int) -> Bool // function type (no [] needed)

<> is never used for generics. < and > are always comparison operators.

Using [] for generics is a deliberate design choice:

  • < and > are always comparison operators, never part of type syntax
  • No ambiguity in expressions like a < b vs f<T>(x)
  • Simpler parsing for both the compiler and LLMs generating code
  • fn foo<T>(x: T) is rejected at parse time
  • Protocols — defining and implementing protocol bounds
  • Types & Values — records, variants, and type aliases
  • Functions — generic functions and type inference