Blocks are Quoin's closures, and they're also how all control flow works — there are no if/while statements, only messages sent to booleans and blocks.
Nav: Foundations · Blocks & control · Objects · Patterns & errors · Concurrency & iteration · Networking & the web · Types · Tooling · Library & reference · Appendices
Rules
- A block is
{ … }. Parameters:{ |a b| … }; type hints (optional):{ |a:Integer b| … }— a hint may be namespaced ({ |e:[Web]Halt| … }); ignore a param with_.- Named block:
{ #name |…| … }attaches a debug name, readable via.name.- Invoke:
.value(0 args),.value:arg(1 arg),.valueWithArgs:#(…)(N args). Also.arity(param count) and.args(param names).- Blocks are closures capturing a live reference to the enclosing scope — later mutations of an outer local are visible inside the block.
- Calling with the wrong number of args is not an error: extra args are ignored; missing params read as
nil. (This may change.)
var double = { |n| n * 2 } double.value:21 "* -> 42 var adder = { |a b| a + b } adder.valueWithArgs:#(3 4) "* -> 7 { |a b| a + b }.arity "* -> 2 { #greet |x| 'hi ' + x }.name "* -> #greet
Closures capture the live environment, so a block can see — and call — names that change after it was created (this is how recursive named blocks work):
var count = 0 var bump = { count = count + 1 } bump.value bump.value count "* -> 2
Other invocation selectors exist for binding a receiver as well as arguments — valueWithSelf:, value:withSelf:, valueWithSelfOrArg: — these are mostly used by the iteration protocol (Part V) to pass each element as both self and the block argument.
Rules
if:,else:,if:else:, andnotare methods defined only ontrueandfalse(seecore/00-bootstrap.qn).nilhas none of them.- Conditionals are strict:
if:/else:/if:else:andwhileDo:require an actual Boolean condition — sending one to a non-Boolean (includingnil) is aMessageNotUnderstood. There is no truthiness coercion for these.- The
if:/else:blocks are zero-arg; they run via.value.- Loops are methods on a block used as the condition:
{ cond }.whileDo:{ body }— re-evaluatescond(must returntrue/false) before each iteration.{ cond }.whileDefinedDo:{ |v| body }— loops whilecond's value isdefined?(non-nil), passing that value into the body.
var score = 95 (score > 90).if:{ 'A'.print } else:{ 'not yet'.print } var x = nil (x == nil).if:{ 'missing'.print } "* compare to produce a boolean first var i = 1 { i <= 3 }.whileDo:{ i.print; i = i + 1 }
Because conditionals are just messages, the receiver of .if: must already be a boolean. Comparison operators (==, <, …) and predicate methods (defined?, contains?:, …) are how you produce one.
⚠ Gotcha —
nil.if:is an error, not "false". Many languages treatnilas falsy; Quoin does not.maybe.if:{ … }throwsMessageNotUnderstoodwhenmaybeisnil(or any non-boolean). Guard with an explicit test:maybe.defined?.if:{ … }or(maybe == x).if:{ … }.
Rules
- A block (and a method body) evaluates to its last expression — no return keyword needed.
^ expr— block return: returns from the current block. At a method's top level the body is the method's block, so^there exits the method; inside a nested block it exits only that block.^^ expr— method return (non-local): unwinds through any intervening blocks and returns from the enclosing method. This is how you break out of an iterator's body.^> expr— yield: sugar forFiber.yield:expr(Part V).
Finder <- { firstBig: -> { |list| list.each:{ |n| (n > 100).if:{ ^^ n } "* ^^ returns from firstBig:, ending the loop }; nil "* fell through: nothing big } } Finder.new.firstBig:#(7 200 9) "* -> 200
Inside the each: block, ^ n would merely end that one iteration of the block (returning n as the block's value, which each: discards) — the loop would continue. ^^ n is what actually exits firstBig:. The standard whileDo: is itself defined using ^^ to unwind its recursion.
Next: Part III — Objects.