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.