Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Programming Idioms

These idioms combine the features introduced in the guide.

Keep native values native

Use C scalar, pointer, struct, union, and enum types when the value’s type is known at compile time. Use Var where the value is dynamic: heterogeneous collection elements, dynamic arithmetic, generic callbacks, or interchange data.

Adopt a protocol for one real contract

Adopt a protocol when a concrete type should support a shared interface. Define the required conversions beside the type. Similar method names are not enough: the operations must have the meanings the protocol requires.

Implement a member directly when the type already provides that operation. Use a base default when every participant value converts to a valid base value and the forwarded operation has the required meaning. Before extending a protocol, use --dump-conformance to check its existing participants.

Box private records through Var(T)

A type does not need to become public because internal code stores it in a Var, List, Array, or Map. Put both conversions beside the private type and adopt the public Var(T) protocol there. Typed arguments, results, assignments, and initializers then box and unbox it without repeated casts.

Keep an explicit value is Type check when a Var came from mixed or untrusted data. Protocol adoption supplies the conversions; it does not check the dynamic type of an arbitrary value.

Make macro calls easier to read than their expansions

A macro should replace repeated source with a call whose meaning is obvious:


macro Statement $guard(Expr $condition) => {
  if (!$condition) return 0;
}

int positive(int value) {
  $guard(value > 0);
  return value;
}

Prefer a function when the job is runtime computation. Prefer direct source when a macro needs substantial compile-time Lisp or AST manipulation to save a few tokens. The macro body, imports, and generated declarations are part of the maintenance cost.

Choose collections by mutation and identity

Use List for immutable sequences that share structure, Array for mutable indexed sequences, and Map for mutable key/value pairs. Build text with Buffer when repeated concatenation would create unnecessary intermediate Strings.

List syntax = %(call print "hello");
Array work = %[1, 2, 3];
Map index = %{name: "x2c"};

work.push(4);
index[<count>] = work.len();

For a closed vocabulary of Symbols that needs membership tests, a dense index, or ordered iteration, declare a SymbolSet literal once and let its perfect-hash index replace a hand-written switch; see Ordered Symbol sets for the literal and its operations.

Destructure small fixed results

Flat List destructuring is clearest when a producer returns a few values in fixed positions:

List bounds = %(0 10 2);
int (start, stop, step) = bounds;

List record = %(7 2.5 "ready");
(int id, float score, String state) = record;

Assignment destructuring returns its source unchanged, so one result can feed several flat views without another call or copy:

Var left_a, left_b, right_a, right_b;
List values = (left_a, left_b) = %(1 2);
(right_a, right_b) = values;

Use match for conditional or nested shapes. Use named accessors when only one position matters.

Let collection methods express collection work

Use foreach when the operation is naturally sequential or can stop early. Use map, filter, and folds for direct transformations:

List values = %(1 2 3 4);
List squares = values.map(%!(item) => item * item);
Var sum = values.foldl(0, %!(total, item) => total + item);

Choose an explicit Iter pipeline when values must stream without first materializing a collection.

Keep lambdas small

An expression lambda is ideal for a short operation with a few captured values. Use a block lambda when the operation needs local declarations, branches, or defer:

List values = %(1 -2 3);
List magnitudes = values.map(%!(Var item) => {
  int value = item.int();
  if (value < 0) return -value;
  return value;
});

Captured scalars are read-only snapshots by default. Use using &name in each lambda that needs to share the binding, including readers and enclosing lambdas. Use a named function and an explicit state object when the operation grows beyond one local task. The collection methods above accept Func, including capturing lambdas. APIs that explicitly take C function pointers still require a noncapturing lambda; see Lambdas.

Separate absence from data

Prefer try_* operations when the full value domain includes Null or when a fallback would lose information:

Map map = %{"name": "x2c"};
Var key = "name";
Var value;
if (!map.try_get(key, &value))
  printf("missing\n");

Sentinel-returning adapters are concise only when the sentinel is impossible in the result domain and the caller does not need a failure reason.

Put cleanup beside acquisition

Scope groups allocations by lifetime. defer runs cleanup when control leaves a block:

Scope.retain();
defer Scope.release();

String path = "/tmp/x2c-idioms.txt";
File output = File.open(path, "w");
defer output.close();

The deferred actions run on ordinary block exit and early transfer. Keep the acquisition and its cleanup close enough to review together.

Raise failures; return ordinary outcomes

Do not turn EOF, a missing Map key, or numeric parse failure into an Error. Those operations already report those outcomes in their results. Use raise for a failure cause that may cross several frames, and put a filtered catch where recovery or reporting belongs. Every intermediate resource owner must have defer or finally cleanup in place.

Prefer the primary API tier

The generated module reference separates:

  • primary operations for ordinary programs;
  • advanced storage, ABI, embedding, and diagnostics APIs;
  • compatibility adapters with a preferred replacement;
  • runtime-internal callables documented only for source readers.

Start with primary APIs; use the others when you need their specific behavior.