Skip to content

Latest commit

 

History

History
142 lines (108 loc) · 6.81 KB

patterns.md

File metadata and controls

142 lines (108 loc) · 6.81 KB

Zerg Patterns & Idioms

How to write the everyday patterns — closures, chained pipelines, and builders — the Zerg way, with the small core it already gives you. Part of the Language Reference. Also in 繁體中文.

Closures & higher-order functions

Zerg's anonymous function fn(…) -> R { … } is the closure — first-class, capturing by copy (see Functions & Closures). There is deliberately no terser |x| lambda: a little verbosity nudges you toward Zerg's procedural-first style rather than deep functional chains. Three ways to keep closures readable, in order of preference:

  1. Name the function — first-class functions pass by name, so a named fn is reusable, testable, and clutter-free at the call site.
  2. Write a for loop — often the clearest, and the most procedural-first.
  3. Inline fn that fits its slot — for a one-off, an untyped parameter takes its type from the function type the closure is checked against, and so does an omitted result type (Type System). Where there is no such position — f := fn (x) { … } — there is nowhere to take either from: the parameter is a named error, and an absent -> type means nil, never an inference.

Chained pipelines

Method calls chain (a.f().g()), so map / filter / fold compose. Prefer named functions over inline closures:

fn double(x: int)   -> int  { return x *% 2 }
fn positive(x: int) -> bool { return x > 0 }

result := xs.map(double).filter(positive).fold(0, add)

[not yet] The adapters themselves do not exist. xs.map(double) reports E9056 NotImplemented: the list method map — this compiler has len and append, and filter and fold answer the same way, so the chain above has nothing to chain. Method calls do chain, and the ones a list has are the two named in that message; until the adapters land, the loop below is not merely the procedural-first alternative but the only spelling.

Or, procedural-first, just loop — the idiom Control Flow endorses (append into another collection):

mut out: list[int] = []
for x in xs {
    continue if not positive(x)
    out.append(double(x))
}

The type is written on the left because the empty list has none of its own — mut out := [] is E3034 the binding out gives the empty list [], which has no type of its own. That matters more here than it usually would: with the adapters unbuilt this loop is the only spelling, so it had better be one that builds.

When an inline function is genuinely one-off, the parameter type its position supplies keeps it short:

ys := xs.map(fn(x) -> int { return x *% 2 })   # x: int taken from xs; -> int written

[not yet] What is left unbuilt in this line is map, marked above as E9056. The untyped parameter is not: x takes its type from the function type the closure is checked against, so the line could be written xs.map(fn(x) { return x *% 2 }) the day the adapter exists.

Builders

Named arguments and default parameters are Zerg's builder. Most fluent Builder().x().y().build() ceremony exists only to make inputs optional — which Functions & Closures already gives you in one call:

fn connect(host: str, port: int = 443, tls: bool = true, timeout: int = 30) -> Conn { … }

c := connect("example.com")                          # all defaults
c := connect("example.com", port: 8080, tls: false)  # override only what you name

For plain data, construction is a call with named fields, which does the same:

cfg := Config(host: "example.com", port: 8080)

When you genuinely need a staged / fluent builder (e.g. a query builder), copy-by-value makes a fluent-immutable builder fall out — each step reads this, modifies a copy, and returns it, so the chain never shares mutable state:

q := new_query().where("age > 18").order("name").limit(10)

# fn where(clause: str) -> Query {
#     mut q := this                # a local copy of the receiver
#     q.filters.append(clause)
#     return q
# }

To mutate the builder in place instead, use a mut fn method on a mut binding (inside a block or with).

Destructuring & pattern support

Destructuring binds directly at a :=: a tuple (a, b) := e and a struct P{x, y} := e both unpack in one step — the everyday way a multiple return or a small record is consumed. The catalogue of what a pattern may be is Control Flow; what matters here is that a target is not a pattern. A match asks whether a value has a shape and a destructuring binding is told it, so nothing in a target tests: every leaf either mints a name (:=) or names storage that already exists (=), and a leaf that would test — a literal — is refused where it is written.

The two shapes nest, and they nest into each other: ((a, b), c) := e and Pair{a: P{x, y}, n} := e. A _ reads a position and drops it. mut and const sit before the whole target, since that is where GRAMMAR#binding puts them, so they apply to every name it mints.

A struct target asks the same three questions a struct pattern does, because it is the same production (GRAMMAR#struct-pat) in another position: the type it names must be the value's, every field it names must exist, and without a .. it names them all.

[not yet] L(Yes(v)) and L(0) are both E9076 NotImplemented: a sub-pattern inside a variant payload, beginning at …. A RESERVED WORD in a payload position is a different rule and keeps its own: L(this) is E2013 this is a reserved word and cannot name a pattern binding.

Binding the whole matched value beside its parts is built — pattern as name, which tests nothing of its own and names what the pattern under it matched. GRAMMAR attaches it to a sub-pattern, which is the SIBLING of a range arm rather than one of its forms, so 1..=9 as n is derived by nothing.

Two more shapes an arm can be written with are refused by name. Matching absence literally is E9068 NotImplemented: nil as a match pattern; and shadowing an arm's own binding inside that arm is E9070 NotImplemented: v re-binds a name a match arm's pattern already binds in this block — the arm's binding is spliced in as text, so the new one would never be read.

Deliberately not added

Two conveniences that other languages reach for are left out to stay small and procedural-first:

  • UFCS (x.f(y) ≡ f(x, y)) would let free functions chain like methods, but adds a name-resolution rule.
  • A pipe operator |> reads well for pipelines but is a new operator and leans functional.

Reach for a named function and a for loop first; they cover these cases without new syntax.