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

Macros by example

Before getting into the technical details of Ion's macro and module system, it will help to be more familiar with the use of macros. We'll step through increasingly sophisticated use cases, some admittedly synthetic for illustrative purposes, with the intent of teaching the core concepts and moving parts without getting into the weeds of more formal specification.

Ion macros are defined using Ion data structures to represent their templates, plus placeholders. In this document, the fundamental construct we explore is the macro definition, denoted using an S-expression of the form (name template) where name must be a symbol denoting the macro's name.

NOTE: Macros can only be defined within directives like set_macros or add_macros, or within modules. We will mostly omit this context in the examples below to keep things simple.

Constants

The most basic macro is a constant:

(pi 3.141592653589793)

This declaration defines a macro named pi. The 3.141592653589793 is the template - just a plain Ion decimal value. Since there are no placeholders in the template, this macro accepts no arguments and always returns a constant value.

To use pi in an Ion document, we write an encoding expression or E-expression:

$ion_1_1
(:pi)

The syntax (:pi) looks a lot like an S-expression. It's not, though, since colons cannot appear unquoted in that context. Ion 1.1 makes use of syntax that is not valid in Ion 1.0—specifically, the (: digraph—to denote E-expressions. Those characters must be followed by a reference to a macro, and we say that the E-expression is an invocation of the macro. Here, (:pi) is an invocation of the macro named pi.

That document is equivalent to the following, in the sense that they denote the same data:

$ion_1_1
3.141592653589793

The process by which the Ion implementation turns the former document into the latter is called macro expansion or just expansion. This happens transparently to Ion-consuming applications: the stream of values in both cases are the same. The documents have the same content, encoded in two different ways. It's reasonable to think of (:pi) as a custom encoding for 3.141592653589793, and the notation's similarity to S-expressions leads us to the term "encoding expression" (or "e-expression").

note

Any Ion 1.1 document with macros can be fully expanded into an equivalent Ion 1.0 document.

We can streamline future examples with a couple of conventions. First, assume that any E-expression is occurring within an Ion 1.1 document; second, we use the relation notation, , to mean "expands to". So we can say:

(:pi) ⇒ 3.141592653589793

Placeholders

Most macros are not constant—they accept inputs that determine their results.

(wrap_value {value: (:?)})

This macro has a template that is a struct containing a single placeholder (:?). The placeholder indicates that the macro accepts one argument. When invoked, the placeholder is replaced with the argument value. Note that a template cannot consist solely of a placeholder (annotated or unannotated) - it must be wrapped in a container or have other content.

(:wrap_value 1)         => {value: 1}
(:wrap_value "foo")     => {value: "foo"}
(:wrap_value [a, b, c]) => {value: [a, b, c]}

The (:?) is a tagged placeholder - it accepts any Ion value including nulls and annotated values.

Simple Templates

Here's a more realistic macro:

(price { amount: (:?), currency: (:?) })

This macro's template is a struct with two placeholders. The macro therefore accepts two arguments when invoked.

(:price 99 USD) ⇒ { amount: 99, currency: USD }

Template expressions that are structs are interpreted almost literally; the field names are literal—which is why the amount and currency field names show up as-is in the expansion—but the field values may contain placeholders.

Templates also treat lists quasi-literally, where each element inside the list may be a literal value or a placeholder. Here's a simple macro to illustrate:

(two_item_list [(:?), (:?)])
(:two_item_list foo bar) ⇒ [foo, bar]

E-expressions can accept other e-expressions as arguments. For example:

(:two_item_list (:price 99 USD) foo)
//              └──────┬──────┘
//                     └─── passing another e-expression as an argument

Expansion happens from the "inside out". The outer e-expression receives the results from the expansion of the inner e-expression.

(:two_item_list (:price 99 USD) foo)

  // First, the inner invocation of `price` is expanded...
  => (:two_item_list {amount: 99, currency: USD} foo)

  // ...and then the outer invocation of `two_item_list` is expanded.
  => [{amount: 99, currency: USD}, foo]

Default Values

Tagged placeholders can provide default values that are used when no argument is provided:

(temperature {
  degrees: (:?),
  scale: (:? K)  // Default value is K
})

When invoking this macro, you can omit the second argument by passing (:) (which means "no argument"):

(:temperature 96 F)   ⇒ {degrees: 96, scale: F}
(:temperature 283 (:)) ⇒ {degrees: 283, scale: K}

Note that when no value is provided for the scale placeholder, it uses the default value K.

E-expressions in Templates

Templates can contain E-expressions, which are expanded when the macro is defined, not when it's invoked. This allows you to use previously defined macros to construct your template.

(:$ion set_macros (prefix_suffix [(:?), (:?)]))
(:$ion add_macros (website_url (:prefix_suffix "https://www.amazon.com/" (:?))))

The website_url macro's template contains an E-expression (:prefix_suffix ...) which is expanded when the macro is defined.

For example:

(:$ion add_macros (website_url (:prefix_suffix "https://www.amazon.com/" (:?))))
  ⇒ (:$ion add_macros (website_url ["https://www.amazon.com/", (:?)]))
(:website_url "gp/cart") ⇒ ["https://www.amazon.com/", "gp/cart"]

Tagless Placeholders

In addition to tagged placeholders, Ion 1.1 supports tagless placeholders that specify a particular type. These are more restrictive but enable more compact binary encoding.

(point {x: (:? {#int}), y: (:? {#int})})

This macro uses tagless placeholders that require integer arguments. The {#int} is an encoding tag indicating the placeholder expects an integer value.

(:point 3 17) ⇒ {x: 3, y: 17}

Tagless placeholders have important restrictions:

  • They cannot accept null values
  • They cannot accept annotated values
  • They cannot be omitted (they're always required)
  • Arguments must match the specified type
(:point null.int 17)   ⇒ // Error: tagless int does not accept nulls
(:point a::3 17)       ⇒ // Error: tagless int does not accept annotations
(:point (:) 17)        ⇒ // Error: tagless parameters cannot be omitted

While Ion text syntax doesn’t use tags—the types are built into the syntax—these errors ensure that a text E-expression may only express things that can also be expressed using an equivalent binary E-expression.

Tagless encodings have no real benefit in text, as primitive types aim to improve the binary encoding.

This density comes at the cost of flexibility. Primitive types cannot be annotated or null, and arguments cannot be expressed using macros.

See tagless_encodings for the complete list of tagless types.

Annotated Macros

Macros can be annotated, which causes the expanded value to have that annotation prepended to any annotations present on the expanded value.

(bar [bar])
(foobar foo::[bar])
baz::(:bar) ⇒ baz::[bar]
baz::(:foobar) ⇒ baz::foo::[bar]

Annotated Placeholders

Placeholders can be annotated, which causes the expanded value to have that annotation prepended to any existing annotations.

(annotated_value [foo::(:?)])
(:annotated_value 42) ⇒ [foo::42]
(:annotated_value bar::42) ⇒ [foo::bar::42]

Argument Evaluation

It's important to understand that arguments to macros are always single Ion values. Even if an argument is a collection (like a list or struct), it's passed as a single value and inserted into the template as-is.

When an E-expression is used as an argument, it is fully expanded before being passed to the macro. The macro receives the result of that expansion, which must be a single Ion value.

(:$ion set_macros
  (wrap_in_list [(:?)])
  (struct_constant {x: 1, y: 2})
)
(:wrap_in_list (:struct_constant)) ⇒ [{x: 1, y: 2}]

In this example, (:struct_constant) is expanded first to produce {x: 1, y: 2}, and then that single struct value is passed to wrap_in_list, which wraps it in a list.

E-expressions in Templates

E-expressions can appear in template definitions, but they are expanded when the macro is defined, not when it's invoked. Additionally, E-expressions may only appear in value position - they cannot appear in field name position in structs.

(:$ion set_macros
  (base_config {type: "standard", size: 100})
  // The following would be an error:
  // (extended_config {
  //   (:base_config),  // ERROR: E-expressions cannot appear in field position
  //   extra: true
  // })
)

Directives

Ion 1.1 includes built-in directives for managing the encoding context. These directives produce system values (not user-visible values) and can only appear at the top level of a stream. See directives.