Tips & Tricks

The following sections provide handy Tips and Tricks to help you effectively build up your own interpreter using custom operators and data types.

Get inspired by checking out the examples

There are quite a few operators and data types available in the TemplateLanguage Example project, under the StandardLibrary class

Also, there are quite a few expressions available in some of the unit tests as well.

Use helper functions to define operators

It’s a lot readable to define operators in a one-liner expression, rather than using long patterns:

infixOperator("+") { (lhs: String, rhs: String) in lhs + rhs }
suffixOperator("is odd") { (value: Double) in Int(value) % 2 == 1 }
prefixOperator("!") { (value: Bool) in !value }

You can find a few helpers in the examples. Feel free to use them!

Be mindful about precedence

Template expressions

The earlier a pattern is represent in the array of statements, the higher precedence it gets. Practically, if there is an if statement and an if-else one, the if-else should be defined earlier, because both are going to match the following expression: {% if x < 0 %}A{% else %}B{% endif %}, but if if goes first, then the output - and the body of the if statement - is going to be processed as A{% else %}B.

Typically, parentheses and richer type of expressions should go earlier in the array.

Typed expressions

The earlier a pattern is represent in the array of functions, the higher precedence it gets. Practically, if there is an addition function and a multiplication one, the multiplication should be defined earlier (as it has higher precedence), because both are going to match the following expression: 1 * 2 + 3, but if addition goes first, then the evaluation would process 1 * 2 on lhs and 3 on rhs, which - of course - is incorrect.

Typically, parentheses and higher precedence operators should go earlier in the array.

Use Any for generics: Variable<Any>

If you are not sure about the allowed input type of your expressions, or you just want to defer that decision until your match is ran and your hit the block in the pattern, feel free to use Variable<Any>("name") in your patterns.

It makes life a lot easier, than definig functions for each type.

Use map on Variables for pre-filtering

Before processing Variable values, there is an option to pre-filter or modify them before it hits the match block.

Examples include data type conversion and other types of validation.

Use OpenKeyword and CloseKeyword for embedding parentheses

Embedding is a common issue with interpreters and compilers. In order to provide some extra semantics to the engine, please use the OpenKeyword("[") and OpenKeyword("]") options, when defining Keywords that come in pairs.

Share context between StringTemplateInterpreter and TypedInterpreter

If you use template interpreters, they need a typed interpreter to hold. Both interpreters have context variables, so if you are not being careful enough, it can cause headaches.

Since Context is a class, its reference can be passed around and used in multiple places.

The reason that the variables are encapsulated in a context is that context is a class, while variables are mutable var struct properties on that object. With this construction the context reference can be passed around to multiple interpreter instances, but keeps the copy-on-write (🐮) behaviour of the modification.

Context defined during the initialisation apply to every evaluation performed with the given interpreter, while the ones passed to the evaluate method only apply to that specific expression instance.

Define constants in Literals

The frameworks allows multiple ways to express static strings and convert them. I believe the best place to put constants are in the Literals of DataTypes.

Use the Literal("YES", convertsTo: true) Literal initialiser for easy definition.

The convertsTo parameter of Literals are autoclosure parameters, which means, that they are going to be processed lazily.

Literal("now", convertsTo: Date())

The now string is going to be expressed as the current timestamp at the time of the evaluation, not the time of the initialisation.

Map any function signatures from Swift, dynamically

The framework is really lightweight and not really restrictive in regards of how to parse your expressions. Free your mind, and do stuff dynamically.

Function(Variable<Any>("lhs") + Keyword(".") + Variable<String>("rhs", interpreted: false)) { (arguments,_,_) -> Double? in
        if let lhs = arguments["lhs"] as? NSObjectProtocol,
            let rhs = arguments["rhs"] as? String,
            let result = lhs.perform(Selector(rhs)) {
            return Double(Int(bitPattern: result.toOpaque()))
        }
        return nil
    }
])

Perform any method call of any type and maybe process their output as well. It’s not the safest way to go with it, but this is just an example.

This opens up the way of running almost any arbitrary code on Apple platforms, from any backend. But, this does it in a very controlled way, as you must define a set of data types and functions that apply, unless you call them dynamically at runtime.

Experiment with your expressions!

It’s quite easy to add new operators, functions, and data types. I suggest not to think about them too long, just dare to experpiment with them, what’s possible and what is not.

You can always add new types or functions if you need extra functionality. The options are practically endless!

Debugging tips

If an expression haven’t been matched

  • It’s common, that some validation caught the value
  • Print your expressions or put breakpoints into the affected match blocks or variable map blocks

If you see weird output

  • Play with the order of the newly added opeartions.
  • Incorrect precedence can turn expressions upside down

The framework is still in an early stage, so debugging helpers will follow in upcoming releases. Please stay tuned!

Validate your expressions before putting them out in production code

Not every expression work out of the box as you might expect. Operators and functions depend on each other, especially in terms of precedence. If one pattern was recognised before the other one, your code might not run as you expected.

Pro Tip: Write unit tests to validate expressions. Feel free to use as! operator to force-cast the result expressions in tests, but only in tests. It’s not a problem is tests crash, you can fix it right away, but it’s not okay in production.