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)
Multi-way conditionals: cond
Use cond for three or more branches instead of nesting if. The =>
operator (precedence 15, left-associative; Unicode alias ⇒) builds a
clause pair. The last list element is the default:
# Prefer: cond for multi-way dispatch
classify(n): cond[n < 0 => "negative", n > 100 => "huge", "normal"]
# Avoid: nested if
classify(n): if(n < 0, "negative", if(n > 100, "huge", "normal"))
Rules:
- Use
condwhen there are three or more branches. - Use
iforthenfor a single two-way test. - Use
whenfor a conditional transform (pass-through if condition is false). condintegrates with the type checker's flow-sensitive narrowing: each clause condition narrows the variable's type within that branch.- The
⇒Unicode alias is accepted everywhere=>is (input via>>in the eucalypt input method).
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).
String building
- Use string interpolation for combining a fixed number of values:
"{pfx}{name}"not[pfx, name] str.join-on(""). - Interpolation auto-converts values — symbols, numbers, etc. No
str.ofneeded:"{:foo}"gives"foo". str.join-onis for joining a list of variable length, not for concatenating two known values.- String anaphora (
•) works inside interpolation:names map("{•}-suffixed").
Call syntax
- Use juxtaposed call syntax with list/block arguments:
f[x, y]notf([x, y]),g{a: 1}notg({a: 1}). - More generally, avoid superfluous parentheses around arguments that are already delimited: lists
[...], blocks{...}, and strings"..."do not need wrapping parens.
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. -
Monadic blocks in lookup chains: a monadic block after
.gets implicit return and sees the LHS bindings. Use parens if you need an explicit return expression in lookup position:ctx.(({ :let ... }.(expr))). -
Capture scope with local bindings: when helpers share parameters, move them inside a block so they capture from scope rather than threading parameters explicitly:
# Before: f threaded through every helper helper(f, x): f(x) + 1 go(f, xs): xs map(helper(f)) # After: f captured from enclosing block go(f, xs): { helper(x): f(x) + 1 }.(xs map(helper)) -
Reuse a name with
:let: an ordinary block binding sees itself on its right-hand side, so{ xs: xs map(f) }is self-reference —xsis the new binding, not an outerxs— which errors with "binding refers to itself" (or loops when the shadowed name is in function position, e.g.{ f: f(x) }). To transform a value and keep its name, use a sequential:letblock, whose RHS sees the outer scope and prior bindings, not itself:normalise(xs): { :let xs: xs map(clean) }.xs # RHS xs is the parameterOtherwise just pick a different local name:
{ cleaned: xs map(clean) }.cleaned. -
Defaulting values from an options block: to fill in specific missing keys in a block from a defaults/options block, use
select+ merge rather than repeatedlookup-orcalls:# Good: select the keys you want defaulted, merge under the input defaults select[:host, :port] config # Avoid: verbose and repetitive { host: config lookup-or(:host, defaults.host) port: config lookup-or(:port, defaults.port) db: config.db }The pattern is
defaults select[<keys-to-default>] input—selectrestricts the defaults block to the keys you want, then merge applies them underneath the input (so input values win).
Prefer when over if for conditional transforms
when(pred?, f, x)appliesfwhenpred?matches, otherwise passesxthrough unchanged. Cleaner thanif(pred?(x), f(x), x)which repeatsx.# Before: x repeated in both branches if(x number?, x + 1, x) # After x when(number?, + 1)
Prefer point-free composition
- Use
bimap,compose(;), and partial application to eliminate intermediate named functions:# Before: named function to combine two transforms on a pair transform[k, v]: [rename(k), process(v)] blk map-elements(transform) # After: bimap composes the two directly blk map-elements(bimap(rename, process))
Avoid nested if for type dispatch
- Nested
if(block?, ..., if(list?, ..., if(symbol?, ...)))chains are hard to read. Prefer:deep-transformfor recursive structural rewriting — it handles the block/list/atom dispatch internallywhenfor single conditional transforms- Named predicates for readability
Sets for membership
- Use sets (
set.from-list) for repeated membership testing, notany(= x)over a list. Faster and reads better with∈:# Before allowed: [:a, :b, :c] allowed any(= v) # After allowed: set.from-list[:a, :b, :c] v ∈ allowed # as expression v when(∈ allowed, f) # section as predicate