Special Forms
When a TDL expression is syntactically an S-expression and its
first element is the symbol .
, its next element must be a symbol that matches either a set of keywords denoting the
special forms, or the name of a previously-defined macro.
The interpretation of the S-expression’s remaining elements depends on how the symbol resolves.
In the case of macro invocations, the elements following the operator are arbitrary TDL expressions, but for special
forms that is not always the case.
Special forms are "special" precisely because they cannot be expressed as macros and must therefore receive bespoke syntactic treatment. Since the elements of macro-invocation expressions are themselves expressions, when you want something to not be evaluated that way, it must be a special form. Argument expressions are passed to the special form without interpretation, and each special form has custom logic for interpreting its arguments.
// The argument being passed in is the _expansion_ of the foo macro
(.regular_macro (.foo))
// This argument being passed in is literally `( '.' 'foo' )`.
(.special_form (.foo))
These special forms are part of the template language itself, and most are not addressable outside TDL;
the E-expression (:if_none foo bar baz)
must necessarily refer to some user-defined macro named if_none
, not to the special form of the same name. The only exception is parse_ion
, which is explicitly included in the system macro table.
literal
(literal (values*) /* Not representable in TDL */)
The literal
form is an identity function that accepts its arguments as literal values and then produces them without any evaluation.
Both literal
and values
are identity functions, but they differ in regard to how their arguments are interpreted:
// When the arguments are values, literal produces the same result as values
(.literal 1 2 3) ⇒ 1 2 3
(.values 1 2 3) ⇒ 1 2 3
// When the arguments are TDL macros or special forms, literal produces different results than values
(.literal (.make_string "a" "b")) ⇒ (.make_string "a" "b")
(.values (.make_string "a" "b")) ⇒ "ab"
// When the arguments are TDL expression groups, literal produces different results than values
(.literal (.. true false)) ⇒ ( .. true false)
(.values (.. true false)) ⇒ true false
// When the arguments are TDL variable expansions, literal produces different results than values
// Assuming that the variable x is bound to "Hello"
(.literal (%x)) ⇒ ( % x )
(.values (%x)) ⇒ "Hello"
if_none
The if_none
special form accepts three arguments—stream
, true_branch
, and false_branch
—each of which may be a single value or a stream of zero-to-many values.
The if_none
form is if/then/else syntax testing stream emptiness.
It has three sub-expressions, the first being a stream to check.
If and only if that stream is empty (it produces no values), the second sub-expression is expanded.
Otherwise, the third sub-expression is expanded.
The expanded second or third sub-expression becomes the result that is produced by if_none
.
note
Exactly one branch is expanded, because otherwise the empty stream
might be used in a context that requires a value, resulting in an errant expansion error.
(macro temperature (degrees scale?)
{
degrees: (%degrees),
scale: (.if_none (%scale) K (%scale)),
})
(:temperature 96 F) ⇒ {degrees:96, scale:F}
(:temperature 283 (::)) ⇒ {degrees:283, scale:K}
To refine things a bit further, trailing optional arguments can be omitted entirely:
(:temperature 283) ⇒ {degrees:283, scale:K}
tip
If you're using if_none
to specify an expression to default to, you can use the default
system macro to be more concise.
(macro temperature (degrees scale)
{
degrees: (%degrees),
scale: (.default (%scale) K),
}
)
if_some
The if_some
special form accepts three arguments—stream
, true_branch
, and false_branch
—each of which may be a single value or a stream of zero-to-many values.
If stream
evaluates to one or more values, it produces true_branch
. Otherwise, it produces false_branch
.
Exactly one of true_branch
and false_branch
is evaluated.
The stream
expression must be expanded enough to determine whether it produces any values, but implementations are not required to fully expand the expression.
Example:
(macro foo (x)
{
foo: (.if_some (%x) [(%x)] null)
})
(:foo (::)) => { foo: null }
(:foo 2) => { foo: [2] }
(:foo (:: 2 3)) => { foo: [2, 3] }
The false_branch
parameter may be elided, allowing if_some
to serve as a map-if-not-none function.
Example:
(macro foo (x)
{
foo: (.if_some (%x) [(%x)])
})
(:foo (::)) => { }
(:foo 2) => { foo: [2] }
(:foo (:: 2 3)) => { foo: [2, 3] }
if_single
The if_single
special form accepts three arguments—stream
, true_branch
, and false_branch
—each of which may be a single value or a stream of zero-to-many values.
If stream
evaluates to exactly one value, if_single
produces the expansion of true_branch
. Otherwise, it produces the expansion of false_branch
.
Exactly one of true_branch
and false_branch
is evaluated.
The stream
argument must be expanded enough to determine whether it produces exactly one value, but implementations are not required to fully expand the expression.
Example:
(macro foo (x)
{
foo: (.if_single (%x) (%x) [(%x)])
})
(:foo (::)) => { foo: [] }
(:foo 2) => { foo: 2 }
(:foo (:: 2 3)) => { foo: [2, 3] }
if_multi
The if_multi
special form accepts three arguments—stream
, true_branch
, and false_branch
—each of which may be a single value or a stream of zero-to-many values.
If stream
evaluates to more than one value, it produces true_branch
. Otherwise, it produces false_branch
.
Exactly one of true_branch
and false_branch
is evaluated.
The stream
argument must be expanded enough to determine whether it produces more than one value, but implementations are not required to fully expand the expression.
Example:
(macro foo (x)
{
foo: (.if_multi (%x) "zero or one" "many")
})
(:foo (::)) => { foo: "zero or one" }
(:foo 2) => { foo: "zero or one" }
(:foo (:: 2 3)) => { foo: "many" }
for
The for
special form maps one or more streams to an output stream.
It accepts two arguments—stream_bindings
and template
.
stream_bindings
is a list or s-expression containing one or more s-expressions of the form (name expr0 expr1 ... exprN)
.
The first value is a symbol to act as a variable name.
The remaining expressions in the s-expression will be expanded and concatenated into a single stream; for each value in the stream, the for
expansion will produce a copy of the template
argument expression with any appearance of the variable replaced by the value.
For example:
(.for
[(word // Variable name
foo bar baz)] // Values over which to iterate
(.values (%word) (%word))) // Template expression; `(%word)` will be replaced
=>
foo foo bar bar baz baz
Multiple s-expressions can be specified. The streams will be iterated over in lockstep.
(.for
((x 1 2 3) // for x in...
(y 4 5 6)) // for y in...
((%x) (%y))) // Template; `(%x)` and `(%y)` will be replaced
=>
(1 4)
(2 5)
(3 6)
Iteration will end when the shortest stream is exhausted.
(.for
[(x 1 2), // for x in...
(y 3 4 5)] // for y in...
((%x) (%y))) // Template; `(%x)` and `(%y)` will be replaced
=>
(1 3)
(2 4)
// no more output, `x` is exhausted
Names defined inside a for
shadow names in the parent scope.
(macro triple (x)
// └─── Parameter `x` is declared here...
(.for
// ...but the `for` expression introduces a
// ┌─── new variable of the same name here.
((x a b c))
(%x)
// └─── This refers to the `for` expression's `x`, not the parameter.
)
)
(:triple 1) // Argument `1` is ignored
=>
a b c
The for
special form can only be invoked in the body of template macro. It is not valid to use as an E-Expression.
parse_ion
Ion documents may be embedded in other Ion documents using the parse_ion
form.
The parse_ion
form accepts a single argument that must be a literal string or blob.
It constructs a stream of values by parsing its argument as a single, self-contained Ion document.
The argument must be a literal value because macros are not allowed to contain recursive calls, and composing an embedded document from multiple expressions would make it possible to implement recursion in the macro system.
The data argument is evaluated in a clean environment that cannot read anything from the parent document. Allowing context to leak from the outer scope into the document being parsed would also enable recursion.
All values produced by the expansion of parse_ion
are application values.
(i.e. it is as if they are all annotated with $ion_literal
.)
The IVM at the beginning of an Ion data stream is sufficient to identify whether it is text or binary, so text Ion can be embedded as a blob containing the UTF-8 encoded text.
Embedded text example:
(:parse_ion
'''
$ion_1_1
$ion::(module _ (symbol_table ["foo" "bar"]]))
$1 $2
'''
)
=> foo bar
Embedded binary example:
(:parse_ion {{ 4AEB6qNmb2+jYmFy }} )
=> foo bar
The parse_ion
form has an address in the system macro table, making it
the only special form that can be invoked as an e-expression.
For normative examples, see parse_ion
in the Ion conformance test suite.