Blocks#

Meta: blocks are doing a lot of work in Dang — they're the iteration protocol, the Ruby-ish DSL hook, the lambda-equivalent, and the body of conditionals/loops. Worth a paragraph naming them explicitly as "the lambda of Dang."

Blocks are the lambda of Dang. There is no separate closure literal or arrow function: a brace block is how code gets passed around, and the same form is the iteration protocol, the hook for Ruby-ish DSLs, and the body of every conditional and loop.

The examples on this page are live: they share one Dang environment, so later snippets use earlier definitions. Each result is computed and baked in by the docs build — edit a snippet and hit Run ▶ to replay the page in your browser.

What a block is#

A block is braces around a sequence of forms, separated by newlines or ,; the last form is the block's result. A bare { ... } with no parameters is itself an expression:

{ let width = 4, let height = 3, width * height }
=> 12

Parameters, when a block takes them, are comma-separated names before a =>:

[1, 2, 3].map { x => x + 1 }
=> [2, 3, 4]

Block arguments to functions#

A function declares a block parameter with the & sigil (the same operator as Functions's &fn refs); its type is a function type. The caller passes the block as trailing braces after the call:

twice(&body: Int!): Int! {
  body + body
}

twice { 21 }
=> 42

The block parameter can itself take arguments — declared &name(params): Ret — and the function body calls it like any function:

apply(&block(x: Int!): String!): String! {
  block(42)
}

apply { x => `got ${x}` }
=> got 42

The block param's arg types may also be a type variable. A type variable is opaque — the body can only pass the value through, not operate on it, so yield * 2 here would be a type error (see Types and nullability):

id(&yield: b): b { yield }

id { "anything" }
=> anything

Regular args and a block param can mix; the block param comes last, and a function or constructor may have at most one:

greet(name: String!, &style(s: String!): String!): String! {
  style(`Hello, ${name}`)
}

greet("Dang") { s => s.toUpper }
=> HELLO, DANG

A block's parameter list can take multiple args when the call supplies them — .each passes the element and its index:

let fruits = ["apple", "banana", "cherry"]

fruits.each { item, index => print(`${index}: ${item}`) }
0: apple 1: banana 2: cherry
=> [apple, banana, cherry]

(.each returns the original list — that's the => line above — so calls chain even when the block is pure side effect.)

Optional parameters#

A block whose body ignores its parameters can omit the param => entirely:

[1, 2, 3].map { "whee" }
=> [whee, whee, whee]

That works anywhere a block does — a constant predicate, say:

fruits.filter { true }     # param ignored ⇒ keeps everything
fruits.filter { false }
=> []

Implicit _ parameter#

Meta: Kotlin's it, not Scala's positional _. The one rule people trip on: every _ in a param-less block is the same one argument.

A block with no explicit params that references _ gets a single implicit parameter named _:

[1, 2, 3].map { _ * 2 }
=> [2, 4, 6]

Every _ in that block refers to that same one argument — Kotlin's it, NOT Scala-style positional _1, _2. So { _ + _ } doubles each element rather than pairing two arguments:

[1, 2, 3].map { _ + _ }
=> [2, 4, 6]

_ binds to the nearest enclosing param-less block, and a nested param-less block shadows the outer one — here the outer _ is each sublist, the inner _ each number:

[[1, 2], [3]].map { _.map { _ * 10 } }
=> [[10, 20], [30]]

A block with explicit params does not capture _{ x => x * 2 } has no implicit parameter — and a bare _ outside any block is an undefined-reference error.

Scoping#

A block is a lexical scope, and let is the way to declare a local. A local let shadows an outer field of the same name; mutating the local leaves the outer untouched:

let name = "outer"
let result = { let name = "inner", name }

[result, name]
=> [inner, outer]

Without a shadowing let, a bare name = value is a reassignment of the existing field, not a new declaration (see Fields) — and that reach extends through nested blocks, with mutations still visible after the block returns. += works on the outer field the same way:

let total = 0
[1, 2, 3].each { x => total += x }

total
=> 6

The same hoisting applies to loop bodies:

let tries = 0
loop {
  tries += 1
  if (tries == 3) { break }
}

tries
=> 3

Inside a block, prefer let for locals; bare (public) declarations are for a type's or module's exported surface, where "public" actually means something.

Control-flow handoff#

Meta: the cute Ruby-esque part. return inside a .map/.each block unwinds the enclosing function, not just the block. The full break/continue spec lives in Control flow; keep only the block-specific wrinkles here.

return inside a block unwinds through the enclosing function, not just the block — which is what makes early exit from an iteration read naturally:

firstMatch(words: [String!]!, prefix: String!): String {
  words.each { w => if (w.hasPrefix(prefix)) { return w } }
  null
}

firstMatch(fruits, "b")
=> banana

break value works inside .each, .map, loop, and user-defined block-arg calls — a block-taking call is a valid target, and break makes it yield the value:

[1, 2, 3, 4].each { x => if (x > 2) { break x } }
=> 3

continue value supplies one iteration's result — in .map it inserts the value; in .each it just advances:

[1, 2, 3].map { x => if (x == 2) { continue 0 }, x }
=> [1, 0, 3]

The value/result rules are specified in Control flow. Two block-specific wrinkles:

When to use a block vs. a function reference#

Dot-block application (piping)#

Meta: this is Dang's piping primitive — there is no |> operator. Lead with the equivalence, then the interleaving, then the null behaviour as a consequence of "application, not navigation."

receiver.{ block } calls the block with the receiver as its single argument — Dang's piping mechanism; there is no |> operator. foo.{ bar(_) }bar(foo), and foo.{ x => bar(x) }bar(foo) (the implicit _ from Implicit _ parameter is the idiomatic form):

inc(x: Int!): Int! { x + 1 }

41.{ inc(_) }
=> 42

It sits at .'s precedence — a sibling of .{{ }} selection and method calls — so it interleaves with real method calls in a single chain:

emphasize(s: String!): String! { `**${s}**` }

"hello world"
  .toUpper
  .{ emphasize(_) }
  .replace(" ", ", ")
=> **HELLO, WORLD**

The block must take 0 or 1 parameters; 2+ is an error: dot-block takes a single value. A 0-param block ignores the receiver and just returns its body:

5.{ "ignored the receiver" }
=> ignored the receiver

Null: dot-block is application, not navigation#

Meta: the null behaviour follows from what dot-block is. Frame it that way rather than as a gotcha vs. selection.

Because foo.{ bar(_) }bar(foo), a null receiver is simply passed in: the block runs with _ bound to null, exactly as bar(null) would. Dot-block applies a block — it does not navigate into the receiver, so it has nothing to short-circuit. This is what lets a block handle null:

let missing = null :: String

missing.{ _ ?? "fallback" }
=> fallback

Contrast .{{ }} selection (GraphQL interop), which is navigation and therefore short-circuits: the selection below never reads name, and its result type is nullable. Same .-brace surface, but selection reads fields while dot-block calls a block:

type User { name: String! }
let nobody = null :: User

nobody.{{name}}
=> null

Common methods that take blocks#