Limbo Overview

You might also like

Download as pdf or txt
Download as pdf or txt
You are on page 1of 7

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 non-
trivial 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.

You might also like