Eucalypt Style Guide
Eucalypt is very flexible and allows you to write extremely ugly code.
Here is some general style guidance for writing eucalypt idiomatically.
List access
head/tailfor list decomposition (variable-length, processing elements sequentially)first/second/!! nfor positional access into fixed-size tuples or records
Don't mix idioms on the same context. If you use second(xs), use first(xs) not head(xs). If you use !! 2, use !! 0 and !! 1 not first and second.
Conditionals
- Prefer simple (unnested) conditionals.
- Use
<bool> then(a, b)overif(cond, a, b)for simple conditionals. - If nesting is unavoidable, use
if, nestedthens are confusing. - Never mix
if()andthen()in the same expression — it looks like they go together and is confusing. - Prefer restructuring to eliminate conditionals altogether: use
nil?guards,max,min, default values (min-of-or,max-of-or), etc. then(false, x)andthen(true, x)are antipatterns. Refactor to disjunctions and conjunctions:cond ∧ x,cond ∨ x, etc. Use block bindings or parentheses to keep∧/∨separated from catenation pipelines:{ ok: xs non-nil? result: xs map(f) sum }.(ok ∧ result > 0)
Catenation precedence
Catenation (juxtaposition / pipeline application) has the lowest operator precedence (20). ALL infix operators bind tighter, including ∧ (35), ∨ (30), = (40), + (75), etc. This means infix operators steal adjacent atoms from catenation pipelines:
xs f(a) + 1parses asxs(f(a) + 1)—+grabsf(a)and1(k > 0) ∧ xs non-nil?parses as((k > 0) ∧ xs) non-nil?—∧grabsxsxs tail ++ [0]parses asxs(tail ++ [0])—++grabstailand[0]
Fix with parens around the catenation: (k > 0) ∧ (xs non-nil?), (xs tail) ++ [0].
Pipelines
- The clearest function definition is a straight pipeline. Structure programs to maximise them.
- Prefer pipeline (catenation) style:
xs headnothead(xs),xs map(f) filter(g)notfilter(g, map(f, xs)). - Use
;(compose) for point-free function definitions and embedding in pipelines:abs ; (+ 1). ∘is also acceptable, particularly in mathematical of strongly FP contexts- Partial application for pipeline steps:
map(area(p)),filter(v-spans(y)).
Parameter order
Choose parameter order to support partial application and pipeline style:
- Put the "data" or "collection" parameter last so that partially applied functions slot into pipelines:
g remove-node("dac") count-paths("svr")reads as a pipeline of transformations. - Put "configuration" or "small" parameters first so they can be fixed early:
map(lookup-count(table)),foldl(dp-step(g), {}, order). - If a function will be used as a
foldlaccumulator, match the(acc, elem)signature:dfs-topo(g, state, node)allowsfoldl(dfs-topo(g), init, nodes).
When in doubt, ask: "how will this function most commonly be called?" and put the varying argument last.
Naming
- Predicates end with
?:vertical?,nil?,at-y. - Use descriptive names:
make-edgesnotmk-edges.
Recursion
- Prefer folds, scans, or specific prelude algorithms over explicit recursion where possible.
Documentation
- Use backtick string metadata (
` "...") for documenting declarations. Backtick metadata attaches to the next declaration, so only use it when the comment is specific to that declaration. - Use
#comments for inline explanatory notes within blocks (e.g. section separators, notes that apply to a group of bindings rather than one declaration) and for disabling code. - Doc metadata should be markdown: use backquotes for
paramandfunctionnames.
Sections and anaphora
- Prefer sections without superfluous brackets:
iterate(+ 2, 0)notiterate((+ 2), 0),map(* 2)notmap((* 2)). - Prefer sections over anaphora when a section suffices:
map(* 2)notmap(_ * 2). - Use anaphora when the expression genuinely needs more than a simple section:
map(_ * _ + 1).
Blocks
- Use blocks for local bindings:
{ x: ... y: ... }.(x + y), but limit to one block, do not stack this construct - Keep block "results"" in
.(...)concise, preferably simple pipelines or expressions, or even just{...}.result - Dynamic generalised lookup: a function can return a block whose names are then used as a namespace for subsequent pipelines, e.g.
prepare(data).( edges take(k) ... ). Use very sparingly — it can defeat static analysis. Never nest or stack dynamic lookups.