Nanyx

Associated types and constraints

Ergonomic generic APIs with context capabilities

Nanyx contexts can model typeclass-style capabilities. For generic APIs, you can pair constraints with associated-type members to avoid noisy signatures.

Why associated types?

A common pain point with generic constraints is having to carry both container type and element type everywhere:

def iterStrings: [Iter(c, a)] c -> () = { items -> ... }

Associated types let you constrain just the container type and derive related types from the context:

context Iter(c) = (
  elem: type
  next: c -> #some(elem, c) | #done
)

Constraining by associated type

Once elem is an associated type on Iter(c), functions can constrain it directly:

def iterStrings: [Iter(c): (elem = string)] c -> () = { items ->
  for items | item -> dbg(item)
}

This keeps signatures compact while preserving full type safety.

Layering collection capabilities

You can build a hierarchy of constraints by composing contexts:

context Iter(c) = (
  elem: type
  next: c -> #some(elem, c) | #done
)

context Sized(c) = (
  length: c -> int
)

context Indexed(c) = (
  elem: type
  get: (c, int) -> elem
)

Then require only what each function needs:

def countItems: [Sized(c)] c -> int = { items ->
  length(items)
}

def headOr: [Iter(c)] (c, Iter(c).elem) -> Iter(c).elem = { items, fallback ->
  match next(items)
  | #some(value, _) -> value
  | #done -> fallback
}

Practical guidance

  • Prefer capability-focused contexts (Iter, Sized, Indexed) over concrete collection constraints.
  • Keep associated types on the context that owns the operation.
  • Avoid over-constraining: require only the operations a function actually uses.
  • Start with simple constraints in public APIs and add richer ones as needed.

For fundamentals, see Contexts. For collection overview, see Collections.