Limbo Overview

Wilfred Springer

1. Introduction
Limbo is the code name of an expression language used in Preon1. It allows you to evaluate expressions on a certain context. The expression language is fairly simple; it supports basic logical and arithmetic operators, and it supports attribute references and item references.

Example 1. Simple Limbo Examples
3 * 4 3 * person.age 2 ^ group.persons[3].age person.age >= 12 ... and that's probably where the correspondence with the JSP Expression Language and other expression languages ends. Because there is a big difference with those languages. And that difference is not based on notation (syntax and grammar), but on the way you apply Limbo. Here are some of the differences of Limbo: • Limbo supports early binding. That is, it will validate the correctness of the expression before it actually validates it. So, when you build the expression, you know if it is an expression that can be evaluated at runtime within a certain context. • Limbo allows you to bind to any context, not just state exposed through bean properties or private fields. • The Limbo API allows you to export the expression as a natural language based human readable snippet of text. Limbo originates from a project to capture dependencies in different fields in a binary media format in an unambiguous way. Using Limbo, that project is capable of generating a human readable description of the encoding format. Expressions like the ones mentioned in Example 1, “Simple Limbo Examples” could be rendered into this:

3 times 4 3 times the age of a person 2 to the power of the age of the third person in the group the age of the person is greater than or equal to 2

This article is a tutorial on Limbo. It will explain the language itself, but it will also explain how you weave it into your own project.

1

Limbo used to be a separate project, however when moving to Preon 2.0, knowing that Limbo is only used inside Preon, it did not seem to make an aweful lot of sense to keep it a separate project, especially with some refactoring that had to be done.

1

Limbo Overview

2. The Language
The first thing you need to know about Limbo is that it is an expression language, intended to execute on a certain context. The context might be a fairly complicated data structure, or it may not. Two things are for sure: Limbo only allows you to refer to things by name or their index, and Limbo does not allow you to define complex structures in the language itself. Let's start with the most commons example: let's look at an simple example object-based data structure. In Figure 1, “Simple data model”, Persons have a name and an age, a father and mother, and optionally some children of their own.

Figure 1. Simple data model

Based on this, here are some example Limbo expressions, given that the context is a person.

age // the age of the current person age >= 35 // the age is greater than or equal to 35 father.age // the age of the father father.age + mother.age >= 70 // the sum of the age of the father and // the age of the mother is greater than //or equal to 70 children[0].age < 7

There are a couple of lessons to learn from the example above. First of all, valid Limbo expressions always evaluate to boolean values or integer values. You wonder why? Well, simply because we did not need anything else. The whole purpose of Limbo is to express arithmetical and logical relationships between things. Producing text simply has never been a requirement. The next thing to notice is that the expressions do not look all that different than Java expressions2. It supports arithmetic operators ('+', '-', '/', '*', '^'), comparison operators ('>', '<', '>=', '<=', '==') and logical operators ('&&', '||', '!'). The third thing to notice is that you can refer to attributes as well as items. (In that sense, Limbo is comparable to Python.) In this example, that works out quite well. Objects also have attributes and some types of objects might have items. However, it is important to remember that Limbo does not bind to objects only. Limbo is able to bind to any model exposed as 'things' with attributes and items. Integer literals can be expressed as decimals (1254), hexadecimals (0xFF, 0xff, etc.) or as binary numbers (0b10101011, 0b1001, etc.). Limbo ignores all whitespace.

2

Notice that I say it does not look all that different. It actually more different than you might expect. More on that somewhere else.

2

Limbo Overview

3. The API

3.1. Getting Started
Let's start with a simple example first:

Example 2. Simple expression
Expression<Integer, Person> doubleAge = Expressions .from(Person.class) .toInteger("age * 2"); Person wilfred = new Person(); wilfred.name = "Wilfred"; wilfred.age = 35; assert 70 == doubleAge.eval(wilfred); In the first line, we build the Expression instance. Since the expression is based on a Person object, the from(...) method takes Person class reference. After that, we specify that we expect an integer result, and pass the expression at the same time. (The builder methods actually have a couple of other options, but we will leave that out for now.) Once the Expression has been built, evaluating is simply calling eval(...) on the expression, passing in the Person instance. And - like you could have expected - the result is 70. Note there is no cast in order to compare to 70, courtesy of the use of generics and auto unboxing.

3.2. The ReferenceContext
I said before that Limbo is capable of binding to anything capable of representing itself as 'things' with named attributes and numbered items. Let me refine that: it is capable of binding to anything for which you can implement a ReferenceContext. So, when in Example 2, “Simple expression” you passed in a Person class, under the hood, Limbo wrapped that inside a ReferenceContext. Now, if you ever used an expression language like the JSP EL, then you are probably aware of a similar mechanism in that expression language. JSP EL has a VariableResolver. Your EL expression can be evaluated against anything, as long as there is a VariableResolver capable of resolving the named things. One of the differences between Limbo's ReferenceContext and JSP EL's VaribleResolver is that the ReferenceContext is parameterized with type of context passed in at evaluation time. Typically, with JSP EL, you will evaluate your expression against a context of of type java.lang.Object. The Java compiler will not be able to verify if the subtype of java.lang.Object you pass in is actually something against which you can evaluate the expression. If you are creating an expression in Limbo, you will always need to construct that expression parameterized ReferenceContext, in which the type parameter is the type of object on which you can apply the expression. So if you have an expression you want to evaluate against an instance of Person, you need to construct the Expression based on a ReferenceContext<Person>.

3

Limbo Overview

Now, you probably wonder why all of that is relevant. What's the purpose of adding the extra complexity of having to deal with parameterized types. After all, the JSP EL works fine with a non-parameterized VariableResolver, and expressions accepting java.lang.Object instances. The real reason for this is that Limbo is capable of early binding. ReferenceContext implementations can make guarantees on the validity of references used in the expression. Which means that the Expression based on that ReferenceContext can guarantee it will be capable of acting upon a certain context. Example 3, “ReferenceContext and References” shows how you build references using a ReferenceContext. In this case, the data model to which we bind is a Java version of the object model outlined in Figure 1, “Simple data model”. The ClassReferenceContext used in this case not only allows you to build references to data contained by an instance of that class, but will also check for the existence of those attributes. Any attempt to reference something that is not defined by the class will generate a BindingException.

Example 3. ReferenceContext and References
ReferenceContext<Person> context = new ClassReferenceContext<Person>(Person.class); Reference<Person> personsName = context.selectAttribute("name"); Reference<Person> fathersName = context.selectAttribute("father").selectAttribute("name"); Person wilfred = new Person(); wilfred.name = "Wilfred"; wilfred.age = 35; Person levi = new Person(); levi.name = "Levi"; levi.age = 8; levi.father = wilfred; assert "Levi".equals(personsName.resolve(levi)); assert "Wilfred".equals(fathersName.resolve(levi)); assert "Wilfred".equals(personsName.resolve(wilfred)); // ... and this will throw a BindingException Reference<Person> gender = context.selectAttribute("gender");

3.3. Natural Language Description
Early validation is not the only benefit we gain from ReferenceContexts supporting early binding. Another benefit is that we basically gather enough information to generate a fairly decent description of the References created. In the example above, the fathersName reference will be printed as: "the name (a String) of the father (a Person) of a Person ". Now, this description might not be ideally suited in your case, but the way your reference is rendered is also determined by the ReferenceContext. You can basically render it any way you like, as long as you are willing to go through the trouble of implementing your own ReferenceContext.

4

Limbo Overview

4. Embedding Limbo
If you ever consider using Limbo, you will most likely do that for its abilities to provide early validation of expressions, and the ability to turn expressions into human-readable descriptions. There is a problem here though. Limbo doesn't define a single ideal way of 'early binding' the expression to a context. You may feel that binding an expression to private inner variables of an object makes perfect sense. Other people will consider that to be malpractice, and require a way to have early validation of expressions bound to getters and setters. So where do you encode these policies? Similarly, Limbo also does not define a single ideal way to turn expressions into human readable language. Of course, you will most likely benefit from Limbo's abilities to turn the main part of the expression into human readable text, but you will probably have a preference for deciding how references should be translated in human readable text. Like, do you want it to be like 'the age of the person', 'the value of the age property of a Person object', or 'person.age'? Again, what do you implement in order to encode these policies? The ReferenceContext is the answer to both of these questions. So basically, embedding Limbo in a context, normally starts by implementing a ReferenceContext. And, in all honesty, that's basically it. Once you start by implementing a ReferenceContext, you will quickly run into having the need to implement various References yourself, you so probably need to take a peek at that interface as well. I currently can't tell you which ReferenceContexts and References you will want to implement. That will be based totally on your own needs. However, I can give you some examples on the use of ReferenceContexts in Preon itself. That should give you a bit of a flavor on what you can do.

5. Limbo in Preon

5.1. BindingsContext
References in expressions used in Preon are not bound to properties. And although references typically resolve to values of (private) fields, the reverse doesn't hold. So, not every (private) field can be addressed by a reference in Preon. In fact, in general only fields that have been marked as 'bound' can be referenced from an expression. Other (private) fields are not even seen. The reason behind this is fairly simple. Preon's objective is to make sure that you can always generate documentation on the encoded representation from the annotated data structure. If the 'specification' (data structure + annotations) would contain references to fields that have been populated outside of Preon's control, then that description would have 'dangling' references; references that point to something of which we don't know anything at all. So essentially, it would leave holes in the documentation. That's where the BindingsContext comes into play. Almost all references in Preon are rooted in the BindingsContext. While constructing the Codec, Preon will construct a BindingsContext for every nontrivial class for which it needs a Codec. So it won't construct a BindingsContext for a Codec decoding a Date, or an Integer, but it will create an instance for your own homegrown class with a couple of 'bound' fields. Now, if you closely examine the ReferenceContext's interface, then you will notice that it has selectAttribute and selectItem methods. When creating a reference to the value of a bound field named 'foobar', then what essentially is happening is that (in case of a named attribute), the selectAttribute is called

5

Limbo Overview

with the name 'foobar'. The BindingsContext will return a Reference object that eventually will resolve correctly into the value of foobar. Remember that Reference itself is not the end of it. A Reference can be used to determine a value at runtime, but it also offers the option of selecting other parts of the data structure by calling one of the Reference's own selectAttribute or selectItem methods.

5.2. ImportSupportingObjectResolverContext
This one definitely deserves an explanation, even it were only because of its incredible long name. In the previous section, I already alluded to the fact that not all references are references to bound fields. Preon uses more than one ReferenceContext, and this one in particular is sometimes wrapped around an existing ReferenceContext in order to make sure you can refer to constants. So this is how it works. If you want to refer to constant values inside your expressions, then you will need to have a way to define those constants. Preon simply allows you to define these constants as you would normally do in Java. However, that does not automatically pull them into scope of your expressions. In order to 'import' these constant definitions, you place an @ImportStatic annotation on top of the class containing references to these constants. The ImportSupportingObjectResolverContext will be instantiated by the ObjectCodecFactory for every class for which it creates a Codec, if and only if that class has the @ImportStatic annotation.

5.3. Index in Offset Expressions
When you use @BoundList, then Preon allows you to specify an offset attribute; that offset attribute can be an expression. The expression allows you to calculate the starting position of an element, given a certain context. Question of course is which element? It turns out that the offset attribute actually introduces a new variable, on top of all variables already in scope. That variable is the 'index'. You can express the offset of an element in terms of that index, by simply referring to that variable. The 'index' variable doesn't come to life just like that. Just like with all of the other examples shown before, it requires a ReferenceContext to make sure the framework understands the existence of this variable. That particular ReferenceContext is also responsible for generating a proper human readable description for that reference in case the framework requires it.

6. Summary
Limbo originally started out as a project to have an expression language catering for the needs of Preon: it required an expression language with APIs for embedding it inside a context that required early binding, and an API for turning expressions into human readable text. The ReferenceContext is one of the central abstractions for having early validation. From the ReferenceContext, you will create References. Those References embody everything there is to know about a reference, including information on how the reference should be rendered into a human-readable descriptive reference.

6

Limbo Overview

Limbo was eventually folded back into Preon as the preon-el module, in order to ease migration to Preon 2.0.

7

Sign up to vote on this title
UsefulNot useful