Blog › ICP guides

Idris developer on retainer: dependent types, Vect n, total function checking, Idris 2 linear types, and proof-carrying code on monthly retainer

October 7, 2026 · ~20 min read

A configuration validation library written in Idris 2 was crashing at runtime on configuration files that contained enum fields with zero valid values. The crash message was Error: empty enum list and the process aborted with no stack trace. The utility function responsible, getFirstEnumValue : List String -> String, was implemented with an explicit empty-list branch: getFirstEnumValue [] = idris_crash “empty enum list”. The library’s documentation said enum fields “must be non-empty”; the type List String in the function signature said nothing of the sort. The Idris developer on retainer changed the type of getFirstEnumValue to require a compile-time proof that the list is non-empty: getFirstEnumValue : (xs : List String) -> (0 prf : NonEmpty xs) -> String. The NonEmpty xs argument is a type-level proof — a value of type NonEmpty (x :: rest) can only be constructed for a cons cell, not for []. The compiler now rejects any call site that passes a List String without either providing IsNonEmpty (for lists known non-empty at construction) or first pattern-matching on the list and handling the empty case before calling the function. Runtime crashes from empty enum fields: 4 per day → 0 over a 30-day monitoring period.

The work log entry read “added NonEmpty proof parameter to getFirstEnumValue, restructured 23 call sites, 11h.” It names the function and the duration, leaving the client unable to explain to the development team why adding one parameter to one function eliminated runtime crashes that had persisted for 8 months of development. The diagnosis required understanding that Idris’s type system can express the constraint “this list is non-empty” as a type, that NonEmpty xs is a proof obligation the compiler enforces statically, and that the 23 call sites each needed to either provide the proof or restructure their control flow to handle the empty case before reaching the function. The 11 hours contained a study of all 23 call sites, a decision on which had structural guarantees of non-emptiness and which needed to be restructured, and the mechanical work of adding proof arguments or pattern-match guards at each site. None of that design work has an artifact in the committed diff beyond the changed function signature and the restructured call sites.

Idris fundamentals: dependent types, Vect n, Fin n, and the type universe hierarchy

Idris is a general-purpose functional programming language with full dependent types: types can contain values, and functions can return types. data Nat : Type where Zero : Nat; S : Nat -> Nat defines natural numbers; Nat itself has type Type. A function can return a Type: someType : Nat -> Type; someType Zero = String; someType (S n) = List Nat is a valid function. The dependent function type (x : T) -> P x is a function from T to a type that depends on the specific value of x; this is strictly more expressive than T -> U where the return type is fixed. The universe hierarchy: Type (also written Type 0) is the type of ordinary types; Type 1 is the type of Type; Idris 2 has a predicative universe hierarchy to avoid paradoxes. In practice, most Idris programs work entirely within Type. The dependent pair (x : T ** P x) is a sigma type: a pair where the type of the second component depends on the value of the first. (n : Nat ** Vect n String) is a vector whose length is bundled with the vector itself — useful when the length is not known at compile time but must be tracked.

Vect n elem is Idris’s length-indexed vector: a list whose length n : Nat is part of the type. Nil : Vect 0 a is the empty vector; (::) : a -> Vect n a -> Vect (S n) a prepends an element, incrementing the length in the type. Because the length is encoded in the type, functions on Vect can be total where the equivalent List function would be partial: head : Vect (S n) a -> a is total because Vect (S n) a cannot be Nil (which has type Vect 0 a), only a cons cell. index : Fin n -> Vect n a -> a is total because Fin n is a type of natural numbers bounded by n: FZ : Fin (S n) is zero; FS : Fin n -> Fin (S n) is the successor. A value of type Fin n cannot represent any number ≥ n, so index i v is always in-bounds. A retainer engagement migrating a data pipeline from List to Vect for fixed-protocol arrays involves identifying which list lengths are known at construction time, adding the length index to the type, and propagating type changes through the pipeline — work that is invisible as a behavioral change because the correct pipeline produces identical results, but eliminates an entire class of potential runtime subscript errors.

Proof witnesses are values that carry compile-time evidence. NonEmpty : List a -> Type; NonEmpty [] = Void; NonEmpty (_ :: _) = Unit defines a type that is Void (uninhabited, no values) for the empty list and Unit (inhabited by ()) for any cons cell. A function that requires a non-empty list can take a (0 prf : NonEmpty xs) parameter — the 0 multiplicity means the proof is erased at runtime and has no runtime cost. The compiler verifies that every call site provides a valid NonEmpty xs proof; since NonEmpty [] = Void has no inhabitants, no proof can be constructed for an empty list, and the call site must handle the empty case before calling the function. Refl : a = a is the identity equality proof; cong : (f : a -> b) -> a = b -> f a = f b lifts equality through a function; sym : a = b -> b = a reverses equality; trans : a = b -> b = c -> a = c chains equalities. the : (ty : Type) -> ty -> ty provides an explicit type annotation: the Int 5 forces 5 to be an Int rather than Nat. Proof-based programming produces changes to function signatures that look mechanical but encode invariants the compiler then enforces forever.

The %total pragma instructs Idris to verify that a function is total: it covers all possible inputs (no missing cases) and terminates (no infinite recursion). Idris checks totality by structural recursion on a decreasing argument: map f [] = []; map f (x :: xs) = f x :: map f xs is total because the recursive call decreases the list argument. A function that recurses without a structurally decreasing argument causes a totality error: Totality: zipWith is not total as it is not purely structural recursive or Totality: function does not reduce on input. Resolving these errors requires adding a size index to the type or using a fuel pattern: fuel : Nat is decremented on each recursive call and the function returns a default value when fuel is exhausted. The %partial pragma marks a function as intentionally partial (suppressing the error); the %covering pragma checks pattern coverage but not termination. A retainer engagement adding %total annotations to a codebase commonly produces 3 to 10 compiler errors per 50 functions, each requiring analysis of the recursion structure and a restructuring decision. The errors identify precisely where the program’s correctness assumptions are not yet formally verified.

Idris 2 linear types, elaborator reflection, interfaces, and backend targets

Idris 2 introduces quantitative type theory with multiplicity annotations on function arguments. (1 x : T) -> U requires that x is used exactly once in the function body; (0 x : T) -> U means x is erased at runtime (used only for type checking); (x : T) -> U or (& x : T) -> U allows unrestricted usage. Linear types enable resource protocols that the compiler enforces: a file handle of type File 1 must be used exactly once — passed to readLine or writeString and then to closeFile, never used twice (duplicated) or discarded (forgotten). The type system prevents double-close and use-after-close errors at compile time rather than at runtime. A network socket protocol can be encoded as a type-level state machine: Socket Closed, Socket Bound, Socket Listening are distinct types; bind : Socket Closed -> Port -> (Socket Bound, ()) transitions from Closed to Bound; calling bind on a Socket Bound is a type error. A retainer engagement designing a linear type resource protocol for a file processing pipeline typically invests 12 to 25 hours in the state machine type design before the first function signature is written; the resulting protocol definitions are compact but encode the complete resource lifecycle.

Elaborator reflection allows Idris programs to inspect and generate Idris syntax during elaboration (the type-checking and proof search phase). %runElab tactic runs an Elab monad action during elaboration; the Elab monad provides operations to inspect the current goal type, generate terms, quote and unquote Idris expressions (quote : a -> TT, unquote : TT -> a), and construct proofs programmatically. A common use: generating interface instances for large product types. A record type with 20 fields requires a 20-case Show instance; an elaborator reflection program can generate it from the field names inspected at compile time. Another use: generating boilerplate for verified data structures where the invariant proof must be propagated through every operation. Elaborator reflection is the mechanism underlying Idris’s most powerful metaprogramming patterns; a retainer engagement designing a %runElab program for compile-time code generation typically involves 15 to 30 hours of design and debugging before the macro produces correct output.

Interfaces in Idris 2 are similar to Haskell type classes: interface Eq a where ((==) : a -> a -> Bool) declares the Eq interface; implementation Eq Nat where (==) Zero Zero = True; (==) (S n) (S m) = n == m; (==) _ _ = False provides an instance. Functor, Applicative, and Monad follow the standard hierarchy. Auto-implicit arguments ({auto prf : SomeConstraint}) are automatically searched by the type checker: if a proof of SomeConstraint can be found in scope, it is inserted automatically without explicit passing. Named implementations allow multiple instances for the same type (%hint marks an implementation as the default search candidate). Prelude provides standard interfaces and functions; Data.Vect, Data.Fin, Data.List.NonEmpty, Data.Maybe, and Data.Either provide the core dependent data structures. Idris 2 compiles to multiple backends: Chez Scheme (the default reference backend), JavaScript/Node.js (for web and server-side), C via RefC (reference-counted C for portable deployment), and a direct C backend. Backend selection affects performance, FFI availability, and deployment constraints; a retainer engagement optimizing an Idris 2 service for deployment commonly involves benchmarking across backends and selecting the appropriate one for the workload.

The IO monad in Idris wraps side-effectful computation: main : IO (); main = putStrLn “Hello”. do notation desugars to >>= binds: do x <- readFile “config.json”; parse x becomes readFile “config.json” >>= parse. The Error monad transformer EitherT stacks error handling over IO: EitherT ConfigError IO Config is a computation that either produces a Config or fails with a ConfigError. Idris 2’s Control.App defines an effect system built on linear types: App [Exception ConfigError, FileIO, Console] Config is a computation that may raise a ConfigError, read files, and write to the console, and returns a Config. The effect system tracks which effects a computation uses; a function marked with only [FileIO] cannot call console output functions without a type error. This is stronger than checked exceptions in Java: the compiler verifies effect usage statically. A retainer engagement designing an App effect system for a validation pipeline commonly involves 8 to 20 hours of effect type design before the handlers are implemented; the design decisions are invisible in the committed function signatures.

How HourTab tracks Idris developer retainer hours

Idris retainer work produces the most extreme version of the invisible-work problem in functional programming. The highest-value changes — adding a NonEmpty xs proof parameter, annotating a function with %total, migrating a List String to Vect 4 String, or designing a linear type state machine for a resource protocol — look trivially small in the diff and eliminate entire categories of runtime failures. A single parameter added to one function signature (0 prf : NonEmpty xs) forces 23 call sites to be restructured and eliminates 4 runtime crashes per day over a 30-day monitoring period. The compiler is now enforcing an invariant that previously existed only in documentation. The 11 hours of call site analysis, proof design, and restructuring are not visible in the diff. The invariant enforcement is permanent and free at runtime because the proof is erased by the 0 multiplicity annotation.

HourTab gives Idris developers a public retainer-hours URL they send to clients — typically formal verification teams, safety-critical systems groups, research computing organizations, or language toolchain developers — at the start of an engagement. The work log is where the technical context lives. For Idris retainers, each entry should name the mechanism involved (%partial annotation audit and total replacement strategy; List.head/List.tail pattern matching restructuring; Vect (S n) elem migration for non-empty guarantees; NonEmpty proof witness addition; Fin n bounded index introduction; %total annotation addition and compiler error resolution; structural recursion restructuring for non-obvious termination; sized type index introduction; fuel pattern design; dependent function type redesign; sigma type proof-carrying data; Refl/cong/sym/trans proof term construction; linear type multiplicity annotation; elaborator reflection %runElab design; interface instance authorship; backend target selection), the specific function name and type signature before and after, the compiler error from %total annotation, the change and why, and the before/after observable metric. Idris retainers are often compared to Haskell developer retainers for type-system-driven correctness work, and to F# developer retainers and Standard ML developer retainers as functional programming engagements where the most critical work is encoding invariants in types. The difference is that Idris’s dependent types can encode invariants that Haskell, F#, and SML’s type systems cannot: the exact length of a collection, the upper bound of an index, the non-emptiness of a list, and the current state of a resource protocol are all expressible in Idris types and checked at compile time. HourTab’s work log bridges the gap: the entry names the invariant, the proof witness type, the call site count, and the before/after crash metric, so the client understands what the retainer accomplished even without knowing dependent type theory.

Track Idris developer retainer hours without the status emails

HourTab gives Idris developers a public URL per client retainer. One link, no login, live burn-down. Your clients stop asking “how many hours do I have left?” and your work log becomes the proof of value that gets the retainer renewed.

See HourTab pricing →

FAQ: Idris developer retainers

What does an Idris developer on retainer typically do?

An Idris developer on monthly retainer provides partial function elimination (%partial annotation audit; List.head pattern matching restructuring; Vect (S n) elem non-empty guarantee migration; NonEmpty proof witness addition; Fin n bounded index introduction for safe total indexing), totality and coverage audit (%total annotation addition and compiler error resolution; structural recursion restructuring; sized type index addition; fuel pattern design; %covering annotation for provably-terminating but automatically-unverifiable functions), dependent type design (Vect n elem length-indexed vector design; Fin n bounded natural number index design; dependent pair (x : T ** P x) sigma type for proof-carrying data; Refl/cong/sym/trans proof term construction; interface Eq/Ord/Show/Functor/Applicative/Monad instance design), and Idris 2 advanced features (linear type multiplicity annotation for resource protocols; elaborator reflection %runElab Elab monad compile-time program generation; backend target Chez Scheme/JS/C/RefC selection; Control.App effect system design).

What Idris work is most underlogged in a retainer?

Partial function audit and NonEmpty proof witness addition (getFirstEnumValue : List String -> String had Nil => idris_crash branch; changed to (xs : List String) -> (0 prf : NonEmpty xs) -> String; 23 call sites restructured; runtime crashes: 4/day → 0 over 30-day period; 8–20 hrs invisible in one function signature change and 23 call site restructurings), %total annotation audit and coverage gap resolution (%total on 14 functions produced 3 compiler errors — three functions had unreachable-in-practice but not-statically-proven-impossible cases; partial function panics/week: 3 → 0; 10–24 hrs invisible in type index additions and case restructuring), and Vect n migration for fixed-protocol arrays (List String replaced with Vect 4 String for guaranteed 4-field parser output; Fin 4 bounded indices replaced defensive bounds checking; subscript errors eliminated at compile time; 8–18 hrs invisible in type annotation changes).

What are typical Idris developer retainer rates?

Entry-level Idris developers (1–2 years, Idris 2 syntax, basic data types Nat/List/Vect/Fin/Maybe/Either, pattern matching, interface instances for Eq/Ord/Show, %total annotation usage) bill at $85–$145/hr. Mid-level Idris engineers (2–4 years, dependent function type (x : T) -> P x design, sigma type (x : T ** P x) construction, Vect n / Fin n migration, NonEmpty proof witnesses, structural recursion restructuring for totality, Idris 2 linear type 1/0 multiplicity annotation for resource protocols) bill at $135–$240/hr. Senior Idris architects (4–8 years, full dependent type system design for safety-critical systems, elaborator reflection %runElab Elab monad program generation, sized type and fuel pattern for complex termination proofs, proof assistant integration, Control.App effect system design) bill at $190–$335/hr. Monthly retainer ranges: $2,500–$6,000/mo for advisory retainers (15–25 hrs), $8,000–$22,000/mo for full dependent types verification engagements.

What should an Idris developer retainer agreement include?

An Idris developer retainer agreement should specify: partial function elimination scope (%partial annotation audit; List.head/tail restructuring; Vect (S n) non-empty guarantee; NonEmpty proof witness addition; Fin n bounded index introduction), totality and coverage scope (%total annotation addition and compiler error resolution; structural recursion restructuring; sized type and fuel pattern design; %covering annotation selection), dependent type design scope (Vect n elem vector design; Fin n bounded index design; dependent pair sigma type; proof term construction; interface instance design), Idris 2 advanced scope (linear type 1/0 multiplicity annotation; elaborator reflection %runElab; backend target selection; Control.App effect system; ipkg package setup), and hour logging format (function name and %partial/%total annotation, proof witness type added, call site count restructured, compiler error text before/after, before/after observable metric).

How should Idris developer retainer hours be logged?

Log each Idris retainer session with: advisory category (%partial annotation audit and total replacement; List.head pattern matching restructuring; Vect (S n) non-empty migration; NonEmpty proof witness signature addition; Fin n bounded index introduction; %total annotation and compiler error resolution; structural recursion restructuring; sized type index addition; fuel pattern design; dependent function type (x : T) -> P x redesign; sigma type proof-carrying data; Refl/cong/sym/trans proof term construction; linear type 1/0 multiplicity annotation; elaborator reflection %runElab design; interface Eq/Ord/Show/Functor/Applicative/Monad instance; backend target Chez Scheme/JS/C/RefC selection), the specific function name and type signature before and after, the compiler error text from %total annotation (cannot find coverage for cases...; not strictly decreasing argument...), change and why (NonEmpty proof parameter required because Idris has no way to statically guarantee List is non-empty without a proof witness — adding the proof forces all call sites to provide evidence or restructure, moving the crash from runtime to compile time; proof is erased at runtime via 0 multiplicity so there is no runtime cost), and before/after metric (runtime crashes from empty enum fields: 4/day → 0; %partial function count: 7 → 0; Fin n subscript errors eliminated at compile time: 12). Include Idris 2 version, backend target, and ipkg package version.