Function Arity Style Guide

Turmeric functions have a hard parameter limit of 16 (MAX_FN_ARITY). Functions with more than ~5 positional parameters are a code smell; 16 is an emergency escape hatch, not a target. This guide covers when to reach for each arity style and the rules each one carries.


Quick decision guide

Situation Reach for
>5 named, independent params defstruct options value
Default values + currying defstruct + (def fast (f defaults))
Unknown number of same-type values & rest :type variadic
Recursive accumulator threading context closure-capture for context; fixed-arity for changing args
Genuinely >16 params Something is wrong -- split the function

More than 5 params -- reach for defstruct

When a function needs many named, independent inputs, pack them into a struct and pass a single options value:

(defstruct CsvOpts
  [delim       : int   ;; field separator (e.g. 44 = ',')
   quote       : int   ;; quote char (e.g. 34 = '"')
   has-header  : int   ;; 1 = first row is header
   infer-rows  : int   ;; rows to sample for type inference
   null-str    : cstr  ;; string that represents NULL (e.g. "")
  ])

(defn read-csv [src : cstr opts : CsvOpts] : int
  ...)

Default values via partial application (Haskell-style idiom):

(def default-csv-opts (CsvOpts 44 34 1 100 ""))

;; read-csv-fast already has opts baked in; call it with just the filename.
(def read-csv-fast (read-csv default-csv-opts))

(read-csv-fast "data.csv")

This composes cleanly with currying: (read-csv default-csv-opts) returns a closure (fn [src :cstr] :int ...) that already has the defaults locked in.


Genuine variadic interfaces -- use & rest :type

When a function takes an unknown number of values of the same type (e.g., println, format, aggregation column lists), use a variadic rest parameter:

(defn println-all [first : cstr  & rest : cstr] : void
  (println first)
  ;; rest is a cons-list of :cstr; walk it with head/tail helpers
  ...)

(println-all "hello")          ;; rest = nil
(println-all "a" "b" "c")     ;; rest = cons("b", cons("c", 0))

The rest type is fully type-checked -- not just primitives. User-defined types (defopaque newtypes, structs, ADTs, type applications) are resolved to their full type and each rest argument is checked by identity at the call site:

(defopaque Route :int)
(defopaque Middleware :int)

(defn launch [& routes : Route] : int ...)

(launch (route!) (route!))     ;; OK -- all Route
(launch (route!) (make-mw))    ;; ERROR: rest arg 1 (expected Route, got Middleware)

Because of this, the old workaround "declare the rest as :int and cast the opaque handles back inside the body" is no longer needed -- write the real type. A bare :int rest now also rejects opaque/struct/ADT values; pass the declared type instead. For a mix of distinct handle types, prefer two explicit :list<T> parameters over a single untyped rest.


Rules for & rest


Walking the rest cons-list in #{Unsafe} code

The rest parameter is a int64_t holding a pointer to a linked list of __tur_cons_cell { int64_t head; int64_t tail; } cells, or 0 (nil). Inline-C helpers that walk it look like:

(defn cons-list-sum [lst : int] #{Unsafe} : int
  ```c
  typedef struct { int64_t head; int64_t tail; } __tur_cons_cell;
  int64_t acc = 0;
  __tur_cons_cell *p = (__tur_cons_cell *)(intptr_t)lst;
  while (p) { acc += p->head; p = (__tur_cons_cell *)(intptr_t)p->tail; }
  return acc;
  ```)

Or use a pure tail-recursive helper:

(defn list-sum-acc [lst : int  acc : int] #{Unsafe} : int
  (if (= lst 0)
    acc
    (list-sum-acc (cons-tail lst) (+ acc (cons-head lst)))))

See also