Skip to content

Functions Generics And Traits

Chris Michael edited this page Sep 2, 2026 · 3 revisions

Pudu

Functions, Generics, And Traits

trait Named {
  fn name(self: &Self) -> Str
}

impl Named for User {
  fn name(self: &User) -> Str { self.name }
}

Status: functions, recursion, first-class function values, defaults, generic types and functions, parameters of higher kind, trait bounds, implementations, qualified and method calls, and dynamic trait values are checked and interpreted.

Functions

fn add(a: Int, b: Int = 1) -> Int { a + b }
fn apply(value: Int, transform: fn(Int) -> Int) -> Int { transform(value) }

Calls evaluate the callee and arguments left-to-right. Default expressions may use module constants and earlier parameters, not caller locals or later parameters. Recursion is implemented; tail-call optimization is not promised.

Generics and bounds

import Std.Order {Ord}

fn smaller[T: Ord](left: T, right: T) -> T {
  if left.before(&right) { left } else { right }
}

Type parameters are explicit on declarations. Multiple constraints use +. Explicit type arguments use name[Type](arguments); omitted arguments are inferred when constraints determine them.

Parameters of higher kind

A parameter normally stands for a type. A parameter written with holes stands for a type constructor — something that becomes a type once it is given arguments. The holes say how many it takes, and they are written in the declaration rather than inferred from use.

T        // a type
F[_]     // a constructor taking one argument, as Array or Option
F[_, _]  // a constructor taking two, as Map or Result

This is what lets a trait be written over the container itself rather than over what it holds:

export trait Mappable[F[_]] {
  fn mapped[A, B](self: &F[A], transform: fn(A) -> B) -> F[B]
}

impl Mappable[Array] for Array {
  fn mapped[A, B](self: &Array[A], transform: fn(A) -> B) -> Array[B] {
    List.map(self, transform)
  }
}

A function may then take "some container" rather than one it was written for:

export fn over[F[_]: Mappable, A, B](values: &F[A], transform: fn(A) -> B) -> F[B] {
  values.mapped(transform)
}

Two rules keep this small. An implementation names a bare constructor — Array, not Array[Int] — and a constructor is never partially applied. And an unbounded parameter of higher kind stays opaque: it says how many arguments its constructor takes and never which constructor it is, so nothing may be read out of it.

Because arity is declared, applying a parameter that did not ask for arguments is reported where it is written. fn swap[F](value: F[Int]) -> F[Str] is E3038, which names the parameter and points at the two things a reader writing it means: a generic type they can name, or a second parameter.

Std.Mappable is the standard library's use of this. It is deliberately small — one member — and exists to show the mechanism carries its weight rather than to import a hierarchy of abstractions.

Traits and implementations

Traits declare behavior without stored state. The orphan rule requires the current module to own the trait or the nominal implementing type. Exact normalized duplicate heads, including alpha-equivalent generic heads, are rejected. General unification-based overlap detection is not implemented. Inherent methods take precedence; ambiguity among applicable visible methods requires qualification.

Named.name(&user)
user.name()

Trait defaults can call other trait methods through Self. Generic trait parameters and implementation-specific associated result behavior are preserved during selection.

Dynamic values

fn render(value: dynamic Named) -> Str { value.name() }

dynamic Trait stores a value whose concrete type is not named at the use site and dispatches through its checked implementation. Ordinary bounded generics remain statically selected.

Current boundary

There is no native monomorphization or ABI yet because native code generation is not implemented. The interpreter provides the observable trait and function behavior described here.

Related

Clone this wiki locally