Synoema

Example Gallery

Language features, algorithms, data structures, and real programs — all with explanations

Language Features

Six constructs that define Synoema's design. Each is chosen for expressiveness and LLM token efficiency.

Pattern Matching

Functions are defined by multiple equations — each with its own pattern. The first matching equation wins. No case, no match, no indentation rules. The compiler warns if patterns are non-exhaustive.

fac 0 = 1
fac n = n * fac (n - 1)

head (x:_)  = x
tail (_:xs) = xs

classify 0 = "zero"
classify n  = ? n > 0 -> "positive" : "negative"

Pipe Operator |>

The |> pipe feeds the result of one function into the next. Data flows left-to-right, stage by stage. Each step is a separate transformation — readable by humans and predictable for LLMs generating code incrementally.

result = [1..20]
  |> filter even
  |> map (\x -> x * x)
  |> foldl (\a b -> a + b) 0
-- 1540

words = ["alice" "bob" "carol"]
  |> map (\s -> "Hello, ${s}!")
  |> map str_len

Algebraic Data Types

Types are defined as a union of constructors. Pattern matching on constructors is total — the language catches missing cases at compile time. No null, no casting, no isinstance.

Shape = Circle Float | Rect Float Float | Point

area (Circle r) = 3.14159 * r * r
area (Rect w h) = w * h
area Point      = 0.0

describe (Circle r) = "circle r=${show r}"
describe (Rect w h) = "rect ${show w}x${show h}"
describe Point      = "point"

Result Type & Error Chains

Functions that can fail return Ok value or Err msg. The and_then combinator chains operations — if any step fails, the error propagates automatically. No try/catch, no null checks in the happy path.

safe_div _ 0 = Err "division by zero"
safe_div a b = Ok (a / b)

-- Chain without nested ifs
compute x =
  a <- safe_div x 2
  b <- safe_div a 3
  Ok (a + b)

main = print (unwrap_or (-1) (compute 12))

Records & Spread Syntax

Records are structural types — create with {}, access with .field. The spread operator ... copies all fields and overrides the ones you name. Immutable by default; updates produce a new record.

user = {name = "Alice", age = 30, role = "user"}

user.name              -- "Alice"
user.age               -- 30

-- Non-destructive update
promoted = {...user, role = "admin"}
older    = {...user, age = user.age + 1}

greet {name, role} = "${name} is ${role}"

Type Inference

Hindley-Milner inference: no type annotations needed, anywhere. Types are inferred globally — including polymorphic functions, ADT patterns, and higher-order functions. Zero annotation overhead for LLMs generating Synoema code.

-- Inferred: Int -> Int
double x = x * 2

-- Inferred: (a -> b) -> a -> b
apply f x = f x

-- Inferred: (a -> b) -> List a -> List b
map f []     = []
map f (x:xs) = f x : map f xs

-- Works with records and ADTs without annotations

Core Algorithms

Quicksort

Quicksort in two recursive equations. The list comprehension [y | y <- xs, y < x] filters elements smaller than the pivot — no temporary variables, no index arithmetic. Compare this to 20+ lines in an imperative language.

qsort [] = []
qsort (x:xs) = qsort [y | y <- xs, y < x]
             ++ [x]
             ++ qsort [y | y <- xs, y >= x]

main = print (qsort [3 1 4 1 5 9 2 6])
-- [1 1 2 3 4 5 6 9]

Factorial

The canonical pattern-matching example. The base case fac 0 = 1 matches exactly when n is zero; the recursive case handles everything else. Tail-call optimized in the compiler — no stack overflow for large inputs.

fac 0 = 1
fac n = n * fac (n - 1)

main = print (fac 10)
-- 3628800

Fibonacci with Memoization

Memoized Fibonacci using an explicit cache tuple threaded through the computation. Synoema's tuple destructuring keeps the accumulator pattern readable without mutation or global state.

fib_go 0 cache = (0, cache)
fib_go 1 cache = (1, cache)
fib_go n cache =
  (a, c1) = fib_go (n - 1) cache
  (b, c2) = fib_go (n - 2) c1
  (a + b, c2)

fib n = fst (fib_go n [])

Binary Search with Contracts

Type annotation plus a requires precondition: the contract is enforced at runtime and extracted into docs by synoema doc --contracts. The function returns Ok index or Err "not found" — never throws.

bsearch : List Int -> Int -> Result Int String
  requires length xs > 0
--- Binary search on sorted list.
--- example: bsearch [1 2 3 4 5] 3 == Ok 2
bsearch xs target = go xs target 0 (length xs - 1)

go xs t lo hi =
  ? lo > hi -> Err "not found"
  : mid = (lo + hi) / 2
    val = index mid xs
    ? val == t -> Ok mid
    : ? val < t -> go xs t (mid + 1) hi
      : go xs t lo (mid - 1)

Data Pipeline

Multi-stage data transformation: each |> step is a named operation. The pipeline reads like a Unix shell command chain — ideal for LLMs that generate one transformation at a time and for humans reading the logic top-to-bottom.

cap_at limit xs =
  map (\x -> ? x > limit -> limit : x) xs

main =
  raw = [15 0 42 8 0 27 100 5]
  raw
    |> cap_at 50
    |> filter (\x -> x > 0)
    |> map (\x -> x * 2)
    |> sum
    |> print
-- 190

Error Handling Chain

Railway-oriented programming: and_then sequences operations that may fail. If any step returns Err, the chain short-circuits and the error propagates to the caller. The happy path reads linearly with no conditional nesting.

parse_int "0" = Ok 0
parse_int "42" = Ok 42
parse_int s    = Err ("not a number: " ++ s)

safe_div a 0 = Err "division by zero"
safe_div a b = Ok (a / b)

parse_and_divide a_str b_str =
  parse_int a_str
    |> and_then (\a -> parse_int b_str
    |> and_then (\b -> safe_div a b))

String Processing

Pure recursive string utilities — no mutable builders. String interpolation ${expr} works with any expression. str_slice, str_len, and str_find are builtins; everything else is defined in the prelude or user code.

str_reverse s =
  ? str_len s <= 1 -> s
  : "${str_reverse (str_slice s 1 (str_len s))}\
    ${str_slice s 0 1}"

str_repeat s 0 = ""
str_repeat s n = "${s}${str_repeat s (n - 1)}"

str_contains haystack needle =
  str_find haystack needle 0 >= 0

Pipeline: CLI Tool

Command dispatch via pattern matching on the argument list. Each pattern is a different command shape — adding a new command means adding one equation, no if-elif chain to extend.

dispatch []                  = print "Usage: tool <cmd>"
dispatch ("greet" : name : _) = print "Hello, ${name}!"
dispatch ("version" : _)     = print "v1.0"
dispatch ("help" : _)        = dispatch []
dispatch _                   = print "Unknown command"

main = dispatch args

Data Structures

Binary Search Tree

A typed BST built from an ADT with three operations: insert, member, inorder traversal. The Tree a type is polymorphic — works for any ordered type without annotations.

Tree a = Leaf | Node (Tree a) a (Tree a)

tree_insert x Leaf = Node Leaf x Leaf
tree_insert x (Node l v r) =
  ? x < v -> Node (tree_insert x l) v r
  : ? x > v -> Node l v (tree_insert x r)
  : Node l v r

tree_inorder Leaf = []
tree_inorder (Node l v r) =
  tree_inorder l ++ [v] ++ tree_inorder r

main = print (tree_inorder
  (foldl (\t x -> tree_insert x t) Leaf
   [5 3 8 1 4 7 9]))

Queue via Two Stacks

An O(1) amortized queue implemented with two lists. The MkQueue constructor wraps the internal state — callers use queue_enqueue and queue_dequeue without knowing the representation.

Queue a = MkQueue (List a) (List a)

queue_new = MkQueue [] []

queue_enqueue x (MkQueue f b) = MkQueue f (x : b)

queue_dequeue (MkQueue [] [])   = Err "empty"
queue_dequeue (MkQueue [] back) =
  queue_dequeue (MkQueue (reverse back) [])
queue_dequeue (MkQueue (x:f) b) =
  Ok (x, MkQueue f b)

State Machine

A traffic-light state machine where each state transition is a one-line equation. Adding a new state means adding one next equation — no switch, no lookup table.

Light = Red | Yellow | Green

next Red    = Green
next Green  = Yellow
next Yellow = Red

run_n state 0 = [show state]
run_n state n = show state : run_n (next state) (n - 1)

main = print (run_n Red 6)
-- ["Red" "Green" "Yellow" "Red" "Green" "Yellow" "Red"]

Records: User Model

Records with field punning, nested access, and spread updates. The {name, age} pattern in greet destructures the record directly into named variables — no intermediate getters needed.

make_user name age role = {name, age, role}

greet {name, role} = "${name} is ${role}"

promote user = {...user, role = "admin"}

birthday user = {...user, age = user.age + 1}

main =
  u = make_user "Alice" 30 "user"
  u2 = promote (birthday u)
  print (greet u2)
-- Alice is admin

Geometry Module

The mod keyword creates a named namespace. use Vec2 (*) imports all public names into scope. Modules are the primary mechanism for organizing larger programs — no separate files needed for small domains.

mod Vec2
  make x y = {x, y}
  add a b  = {x = a.x + b.x, y = a.y + b.y}
  dot a b  = a.x * b.x + a.y * b.y
  len_sq v = v.x * v.x + v.y * v.y

use Vec2 (*)

p1 = make 3 4
p2 = make 1 2
main = print (len_sq (add p1 p2))
-- 52

Concurrency & Async

Stackless async/await with a mio epoll/kqueue reactor. async fn and await run on the JIT path — no callback pyramids, no thread-per-connection.

async fn / await

Async functions are stackless coroutines. await suspends the current task without blocking an OS thread — the reactor reschedules it when the result is ready. Works in both interpreter and JIT modes.

async fn delayed name =
  _ = await (async_sleep 50)
  "Hello, " ++ name

async fn main =
  a = await (delayed "Alice")
  b = await (delayed "Bob")
  print (a ++ " & " ++ b)
-- Hello, Alice & Hello, Bob

race / gather

race runs tasks in parallel and returns the first result — the rest are dropped. gather runs all tasks in parallel and collects every result in order. Both are built-in; no external executor needed.

async fn fast = 1
async fn slow = _ = await (async_sleep 500); 2

async fn main =
  winner = await (race [slow fast])
  print winner          -- 1 (fast wins)

  all = await (gather [fast fast fast])
  print (show all)      -- [1, 1, 1]

scope_result / try_await

scope_result runs an async block and captures any error call as Err — the program continues instead of crashing. try_await does the same for a single task. Essential for LLM-generated code that may produce runtime errors.

async fn risky n =
  ? n == 0 -> error "zero input"
  : n * 2

async fn main =
  r = scope_result (\() -> await (risky 0))
  ? is_ok r -> print "ok"
  : print ("caught: " ++ show (unwrap_err r))
  -- caught: zero input

  safe = try_await (risky 5)
  print (show safe)     -- Ok 10

Async TCP Server

Non-blocking TCP server in 8 lines. async_tcp_listen binds the port; async_tcp_accept waits for a client without blocking. The mio reactor handles epoll/kqueue — no OS thread is consumed per connection.

async fn handle client =
  data = await (async_tcp_read client)
  await (async_tcp_write client ("echo: " ++ data))
  await (async_tcp_close client)

async fn main =
  listener = await (async_tcp_listen 9877)
  client   = await (async_tcp_accept listener)
  await (handle client)
-- Test: echo "hello" | nc 127.0.0.1 9877

Real Programs

HTTP Server with Concurrency

A concurrent HTTP server in 9 lines. scope starts a supervised concurrent region; spawn runs each connection handler in parallel. The server loop never blocks — each client gets its own fiber.

handle fd =
  req = fd_readline fd
  fd_write fd "HTTP/1.0 200 OK\r\n\r\nHello!"
  fd_close fd

main = scope
  listener = tcp_listen 8080
  loop l =
    client = tcp_accept l
    spawn (handle client)
    loop l
  loop listener

JSON Processing

json_parse returns Ok value or Err msg. json_get key obj extracts a field — also a Result. Chaining with and_then gives a safe, composable JSON pipeline without any exception handling.

get_field key obj = unwrap (json_get key obj)

transform src =
  parsed = unwrap (json_parse src)
  name   = get_field "name" parsed
  age    = get_field "age" parsed
  "User: ${name}, age ${show age}"

main =
  src = "{\"name\": \"Alice\", \"age\": 30}"
  print (transform src)
-- User: Alice, age 30

HTTP Client

http_get returns Ok body on success or Err msg on failure. Pattern matching on the result directly — no try/catch wrapper, no status-code parsing. Works in the JIT path for high-throughput scripts.

fetch url =
  result = http_get url
  ? result
    -> Ok body -> print body
    -> Err msg -> print "Error: ${msg}"

main =
  fetch "http://httpbin.org/get"

List Operations

zip_with: pairwise combine

zip_with applies a binary function element-by-element across two lists — shorter list determines the length. A concise alternative to indexed loops.

main = print (zip_with (+) [1 2 3 4 5] [10 20 30 40 50])
-- [11 22 33 44 55]

dot_product xs ys = foldl (+) 0 (zip_with (*) xs ys)
main = print (dot_product [1 2 3] [4 5 6])
-- 32

zip: pair two lists

zip produces a list of pairs, stopping at the shorter input. Useful for combining parallel data without index arithmetic.

names  = ["Alice" "Bob" "Carol"]
scores = [92 87 95]
ranked = zip names scores
-- [("Alice",92) ("Bob",87) ("Carol",95)]

main = print (map (\p -> "${fst p}: ${show (snd p)}") ranked)
-- ["Alice: 92" "Bob: 87" "Carol: 95"]

take / drop: list slicing

take n returns the first n elements; drop n skips them. Together they implement any slice or pagination pattern without index math.

xs = [1 2 3 4 5 6 7 8 9 10]
page n size = take size (drop (n * size) xs)

main =
  print (take 3 xs)       -- [1 2 3]
  print (drop 7 xs)       -- [8 9 10]
  print (page 1 3)        -- [4 5 6]

sort: natural ordering

sort works on any type with a natural order — Int, Float, String. Returns a new list; the original is unchanged.

main =
  print (sort [3 1 4 1 5 9 2 6 5])   -- [1 1 2 3 4 5 5 6 9]
  print (sort ["banana" "apple" "cherry"])
  -- ["apple" "banana" "cherry"]

reverse

Pure list reversal. Combine with == for palindrome checks, or use as the final step in an accumulator fold.

palindrome s = s == reverse s

main =
  print (palindrome "racecar")  -- true
  print (palindrome "hello")    -- false
  print (reverse [1 2 3 4 5])   -- [5 4 3 2 1]

nub: remove duplicates

nub removes duplicate elements, preserving the first occurrence. O(n²) but zero-allocation — appropriate for small lists.

tags = ["rust" "wasm" "rust" "llm" "wasm" "synoema"]
unique_tags = nub tags
-- ["rust" "wasm" "llm" "synoema"]

counts = map (\t -> (t, length (filter (\x -> x == t) tags))) unique_tags

partition: split by predicate

partition returns a pair of lists: elements satisfying the predicate, and those that don't. Single pass — more efficient than two separate filters.

Pair a b = MkPair a b
fst (MkPair a _) = a
snd (MkPair _ b) = b

result = partition even [1 2 3 4 5 6 7 8]
evens = fst result   -- [2 4 6 8]
odds  = snd result   -- [1 3 5 7]

chunks_of: fixed-size groups

chunks_of n splits a list into non-overlapping groups of size n. The last group may be smaller than n.

matrix = chunks_of 3 [1 2 3 4 5 6 7 8 9]
-- [[1 2 3] [4 5 6] [7 8 9]]

-- process rows in batches of 5
batch_process xs = map process (chunks_of 5 xs)

intersperse: insert separators

intersperse sep xs inserts sep between each pair of elements. The string version uses intercalate to join with a string separator.

main =
  print (intersperse 0 [1 2 3 4])    -- [1 0 2 0 3 0 4]
  print (intercalate ", " ["a" "b" "c"])   -- "a, b, c"
  csv = intercalate "," ["name" "age" "role"]
  print csv    -- "name,age,role"

flatten: nested list collapse

flatten collapses one level of nesting. Combine with map (i.e. concatMap) to expand and flatten in one pass.

groups = [[1 2 3] [4 5] [6 7 8 9]]
flat   = flatten groups    -- [1 2 3 4 5 6 7 8 9]

-- concatMap = flatten . map
expand x = [x x*2 x*3]
main = print (concatMap expand [1 2 3])
-- [1 2 3 2 4 6 3 6 9]

Math Builtins

sin / cos / pi

Transcendental functions operate on Float. pi is the built-in constant. Results are identical between interpreter and JIT.

main =
  print (sin (pi / 6))    -- 0.5
  print (cos 0.0)         -- 1.0
  print (sin (pi / 4))    -- 0.7071...
  circle_area r = pi * r * r
  print (circle_area 5.0) -- 78.539...

mean: arithmetic average

mean computes the arithmetic mean of a Float list. Returns 0.0 on an empty list. Works in both interpreter and JIT.

scores = [82.0 91.0 74.0 88.0 95.0 79.0]
avg    = mean scores    -- 84.833...

-- weighted mean: zip weights and values
weighted_mean ws vs =
  total_w = foldl (+) 0.0 ws
  total_v = foldl (+) 0.0 (zip_with (*) ws vs)
  total_v / total_w

stddev: sample standard deviation

stddev uses Bessel's correction (n−1 denominator) matching R's sd(). Returns 0.0 for lists with fewer than 2 elements.

temperatures = [22.1 23.4 21.8 24.0 22.7]
spread       = stddev temperatures   -- ≈ 0.88

-- coefficient of variation (relative spread)
cv xs = stddev xs / mean xs * 100.0

exp / ln: exponential and natural log

exp x computes e^x; ln x computes the natural logarithm. They are inverses: ln (exp x) == x.

main =
  print (exp 1.0)          -- 2.71828...  (e)
  print (ln (exp 3.0))     -- 3.0
  print (exp 0.0)          -- 1.0

-- continuous compounding: A = P * exp(r * t)
compound p r t = p * exp (r * t)

atan2: two-argument arctangent

atan2 y x returns the angle in radians in the correct quadrant. Essential for vector direction, physics, and coordinate transformations.

-- angle of vector (3, 4)
angle = atan2 4.0 3.0    -- ≈ 0.927 radians

-- convert to degrees
to_deg r = r * 180.0 / pi
main = print (to_deg (atan2 1.0 1.0))  -- 45.0

log10: base-10 logarithm

log10 x is the base-10 log. Use log2 for bit-width calculations. For an arbitrary base: log_b b x = ln x / ln b.

main =
  print (log10 1000.0)   -- 3.0
  print (log10 100.0)    -- 2.0
  print (log2 1024.0)    -- 10.0

-- decibel conversion
db power_ratio = 10.0 * log10 power_ratio

Contracts

requires: precondition guard

A requires annotation on a function asserts a precondition on the inputs. Violated at call-site → runtime error with the contract message. Extracted to docs by synoema doc --contracts.

sqrt_safe : Float -> Float
--- requires: x >= 0.0
sqrt_safe x = x ** 0.5

-- Calling sqrt_safe (-1.0) raises:
-- contract violation: requires: x >= 0.0

ensure: postcondition assertion

An ensure clause asserts a property of the return value. The keyword result refers to the actual returned value. It is written as an indented line under the type signature.

abs_val : Int -> Int
  ensure result >= 0
abs_val n = ? n < 0 -> n * (-1) : n

normalize : List Float -> List Float
  ensure length result == length xs
normalize xs = map (\x -> x / mean xs) xs

requires + ensure: full contract

Combining both clauses gives machine-verifiable pre/post conditions. The compiler extracts the full contract into documentation.

divide : Int -> Int -> Int
  requires b != 0
  ensure result * b == a
divide a b = a / b

bsearch : List Int -> Int -> Result Int String
  requires sort xs == xs
  ensure is_ok result -> index (unwrap result) xs == target
bsearch xs target = go xs target 0 (length xs - 1)

contract extraction CLI

Run synoema doc --contracts file.sno to generate a compact table of all annotated functions. Works with MCP tool doc_query for agent-side retrieval.

-- Run: synoema doc --contracts examples/bsearch.sno
-- Output:
-- | Function | Requires             | Ensure                    |
-- |----------|----------------------|---------------------------|
-- | bsearch  | sort xs == xs        | index (unwrap r) xs == t  |
-- | divide   | b != 0               | result * b == a           |

IoT Rules

Threshold rule with contract

The IoT rules subset uses integer arithmetic only (no Float) for Tier 0 MCU targets. The requires clause constrains sensor range; ensure asserts output is binary.

fan_control : Int -> Int
  requires temp >= -40 && temp <= 125
  ensure result == 0 || result == 1
fan_control temp = ? temp > 30 -> 1 : 0

main =
  print (fan_control 35)   -- 1  (fan on)
  print (fan_control 22)   -- 0  (fan off)

Hysteresis rule: dual-threshold

Hysteresis prevents rapid on/off switching near the threshold. The rule only changes state when crossing the high threshold on the way up or the low threshold on the way down.

heater_hyst : Int -> Int -> Int
  requires low < high
heater_hyst state temp =
  ? state == 0 && temp > 22 -> 1
  : ? state == 1 && temp < 18 -> 0
  : state

main =
  s0 = heater_hyst 0 25   -- 1  (turn on at 25)
  s1 = heater_hyst 1 20   -- 1  (stay on, above low threshold)
  s2 = heater_hyst 1 17   -- 0  (turn off below 18)

Interlock rule: multi-sensor safety

Interlocks require multiple conditions to be simultaneously true before allowing an action. Critical in industrial safety — no single sensor can trigger an unsafe state.

valve_interlock : Int -> Int -> Int -> Int
  requires pressure >= 0 && flow >= 0
  ensure result == 0 || result == 1
valve_interlock pressure flow override =
  ? override == 1 -> 0
  : ? pressure < 50 && flow > 10 -> 1
  : 0

Timer / accumulator rule

Timer rules accumulate ticks and trigger after a threshold is crossed. Integer-only, safe for Tier 0 MCUs with no floating-point unit.

inactivity_alert : Int -> Int -> Int
  requires ticks >= 0 && threshold > 0
  ensure result == 0 || result == 1
inactivity_alert ticks threshold =
  ? ticks >= threshold -> 1 : 0

-- step the timer: reset on motion, increment on idle
step motion ticks = ? motion == 1 -> 0 : ticks + 1

String & Text

Word frequency counter

Build a frequency map by folding over a list of words. map_get returns a default value on missing key — no explicit "not found" handling needed.

count_words words =
  foldl (\m w -> map_insert w (map_get w 0 m + 1) m) map_empty words

main =
  text   = ["the" "cat" "sat" "on" "the" "mat" "the"]
  counts = count_words text
  print (map_lookup "the" counts)  -- Ok 3
  print (map_lookup "cat" counts)  -- Ok 1

ROT13 transformation

A character-level string transformation using str_map. Demonstrates that ROT13 is self-inverse: applying it twice returns the original.

rot13_char c =
  ? c >= 65 && c <= 90  -> ((c - 65 + 13) % 26) + 65
  : ? c >= 97 && c <= 122 -> ((c - 97 + 13) % 26) + 97
  : c

rot13 s = str_map rot13_char s

main =
  enc = rot13 "Hello, World!"    -- "Uryyb, Jbeyq!"
  print (rot13 enc)              -- "Hello, World!"

intercalate: join with separator

intercalate sep xss joins a list of strings (or lists) with a separator — the string equivalent of Python's str.join.

main =
  print (intercalate ", " ["Alice" "Bob" "Carol"])
  -- "Alice, Bob, Carol"

  path_parts = ["usr" "local" "bin" "sno"]
  print (intercalate "/" path_parts)
  -- "usr/local/bin/sno"

  csv_row fields = intercalate "," fields

Palindrome check

A one-liner using reverse and structural equality. Works for both strings and lists.

is_palindrome s = s == reverse s

main =
  print (is_palindrome "racecar")    -- true
  print (is_palindrome "level")      -- true
  print (is_palindrome "synoema")    -- false
  print (is_palindrome [1 2 3 2 1])  -- true

Type Patterns

Maybe: optional values without null

Synoema has no null. Optional values are expressed with Just x / None. Chain optional operations with map_maybe and from_maybe — no null checks, no exceptions.

Maybe a = Just a | None

safe_head []     = None
safe_head (x:_)  = Just x

safe_div _ 0 = None
safe_div a b = Just (a / b)

main =
  print (from_maybe (-1) (safe_div 10 2))   -- 5
  print (from_maybe (-1) (safe_div 10 0))   -- -1
  print (safe_head [3 1 4])                 -- Just 3

Mutual recursion: even / odd

Two functions that call each other. Synoema resolves mutually recursive definitions in the same file without any forward-declaration syntax.

is_even 0 = true
is_even n = is_odd (n - 1)

is_odd 0 = false
is_odd n = is_even (n - 1)

main =
  print (is_even 10)   -- true
  print (is_odd 7)     -- true
  print (map is_even [0 1 2 3 4 5])
  -- [true false true false true false]