Style
Idiomatic Yona is not just aesthetics: several of these rules change what the
compiler can do for you. Flat let bindings parallelize; nested ones serialize.
Each rule below shows the bad form, the good form, and why.
Never nest let
Section titled “Never nest let”let takes multiple comma-separated bindings; nesting buries that and hurts
readability.
# Bad — unnecessary nestinglet x = 42 inlet y = x + 1 inx + y# Good — flat multi-bindinglet x = 42, y = x + 1 in x + y# => 85The payoff is bigger than style: independent bindings in one let run in
parallel. Nested lets force sequential execution even when the bindings
don’t depend on each other.
# Bad — each read waits for the previous onelet a = readFile "foo.txt" inlet b = readFile "bar.txt" ina ++ b# Good — both reads in flight at once; elapsed ≈ max, not sumlet a = readFile "foo.txt", b = readFile "bar.txt"in a ++ bSee Concurrency for the full model.
let and do have different semantics
Section titled “let and do have different semantics”let binds values. Independent right-hand sides may run in parallel and are
awaited at first use. do sequences effects: every step runs strictly top to
bottom, even when the steps look independent. Combining them is valid — and
often the right shape — when you want both:
# Good — two reads in flight, then ordered writeslet a = readFile "foo.txt", b = readFile "bar.txt"in do writeFile "out-a.txt" (process a) writeFile "out-b.txt" (process b)endPutting those reads in a do would serialize them. Putting those writes in a
multi-binding let would allow them to overlap. Use let when the bindings are
values (and may run together); use do when order itself is the point. See
Concurrency.
The anti-pattern is using let as a sequencer for an unused effect:
# Bad — discard-binding to force an effectlet _ = writeFile "out.txt" data in data# Good — do block for side effects; last expression is the valuedo writeFile "out.txt" data dataenddo also takes intermediate bindings (name = expr), executed strictly in
order — the idiomatic shape for a protocol or any sequential I/O where the steps
depend on each other:
do content = readFile "input.txt" result = process content writeFile "output.txt" result resultendDo not wrap a single expression in do. Do not pad a function or program with a
dummy last value such as 0 — the last real expression is the result
(println already yields ()).
Comma-separate imports
Section titled “Comma-separate imports”Nested import expressions add a level of indentation per module for no
benefit.
# Bad — one import wrapping anotherimport length from Std\String inimport println from Std\Io inprintln (length "hello")# Good — one import expression, comma-separated clausesimport length from Std\String, println from Std\Io inprintln (length "hello")# 5Use with for resources
Section titled “Use with for resources”Manual close calls are lost on every early exit and exception; with releases
the resource deterministically when the scope exits.
# Bad — close is skipped if send raisesdo fd = tcpConnect "localhost" 8080 send fd "hello" close fdend# Good — released on success or exception, checked by the Closeable traitwith fd = tcpConnect "localhost" 8080 in send fd "hello"The resource type must implement Closeable — this is verified at compile time,
so a with over a non-resource is an error, not a surprise at runtime.
Parallel comprehensions for concurrent work
Section titled “Parallel comprehensions for concurrent work”Mapping an I/O-bound or CPU-heavy function sequentially wastes the runtime’s
thread pool; [| … ] runs one task per element and keeps result order.
# Bad — one fetch at a time[ httpGet url for url = urls ]# Good — all fetches concurrent, results in source order[| httpGet url for url = urls ][| x * 2 for x = [1, 2, 3, 4, 5] ]# => [2, 4, 6, 8, 10]Keep the plain form [ … ] for cheap pure bodies, where task overhead would
exceed the work.
foldl for aggregation
Section titled “foldl for aggregation”Hand-rolled non-tail recursion over a sequence grows the call stack;
Std\List.foldl is tail-recursive, which the compiler turns into a loop, so it
cannot overflow.
# Bad — deep recursion, stack depth proportional to lengthlet sum xs = case xs of [] -> 0 [h|t] -> h + sum tend in sum bigList# Good — foldl, constant stackimport foldl from Std\List infoldl (\acc x -> acc + x) 0 bigListimport foldl from Std\List infoldl (\acc x -> acc + x) 0 [1, 2, 3, 4]# => 10foldr exists for the cases that genuinely need right association; default to
foldl.
Iterators for streaming
Section titled “Iterators for streaming”Reading a whole file into a sequence costs O(file) memory; iterator-based
functions like readLines, chars, and split stream in O(1).
# Bad — reads the whole file into one string before countingimport readFile from Std\File, chars from Std\String, foldl from Std\List infoldl (\n c -> if c == '\n' then n + 1 else n) 0 [c for c = chars (readFile "big.log")]# Good — streams line by line, constant memoryimport readLines from Std\File, foldl from Std\List infoldl (\n _ -> n + 1) 0 [line for line = readLines "big.log"]Iterator values feed comprehensions as generator sources; nothing is read from the file until the generator pulls it.
Naming conventions
Section titled “Naming conventions”Consistent casing carries information: you can tell a constructor from a function from a symbol at a glance.
# Badprocess_item x = x # snake_case functionmodule std\my_utils # lowercase modulecase status of :OK -> 1 end # uppercase symbol# GoodprocessItem x = x # camelCase functions and variablesmodule Std\MyUtils # PascalCase modules, backslash-separatedcase status of :ok -> 1 end # :snake_case symbols- Functions, variables:
camelCase—readFile,processItem - Modules, types, constructors:
PascalCase—Std\String,Option,Some - Symbols:
:snake_case—:ok,:not_found - Type variables: single lowercase letters —
a,b,e
Indentation
Section titled “Indentation”Two spaces per level, lines within 80–100 characters.
# Bad — four spaces and tab mixes drift into misalignmentcase xs of [] -> 0 [h|t] -> hend# Good — two spacescase xs of [] -> 0 [h|t] -> hendNewlines end expressions in case arms, do blocks, and module bodies, so
consistent shallow indentation keeps expression boundaries obvious. Inside
brackets, and after binary operators and ->, newlines are suppressed — use
that for natural line continuation instead of escape characters.
Use the prelude
Section titled “Use the prelude”Some, None, Ok, Err, Linear, Iterator, identity, const, flip,
and compose are always in scope; importing or re-defining them is noise.
(Collection functions like foldl are not prelude — import them from
Std\List.)
# Bad — shadowing a prelude type with a homemade onetype Maybe a = Just a | Nothingcase lookup k m of Just v -> v; Nothing -> 0 end# Good — prelude Option, no import, no declarationcase lookup k m of Some v -> v None -> 0endPrefer Result a e (Ok/Err) for fallible operations and Option a
(Some/None) for absence; both pattern-match everywhere without setup.
Quick checklist
Section titled “Quick checklist”- Flat
let, one binding list — independent bindings parallelize. letfor values,dofor ordered effects; combining them is fine. Neverlet _ = effect, never a one-linedo, never a dummy trailing0.- One
import, comma-separated clauses. withfor anythingCloseable.[| … ]when the body is worth a task;[ … ]otherwise.Std\List.foldlover hand-rolled recursion for aggregation.- Iterators for large inputs.
camelCase/PascalCase/:snake_case; two-space indent.- Reach for the prelude before writing it yourself.
For the semantics behind these rules, see Concurrency, Modules, and the specification.