View on GitHub

Computation-engine

A Scala library for evaluating sequences of computations written as Scala strings, on domains of arbitrary facts.

Download this project as a .zip file Download this project as a tar.gz file

"You can build a simple rules engine yourself. All you need is to create a bunch of objects with conditions and actions, store them in a collection, and run through them to evaluate the conditions and execute the actions." -- Martin Fowler, "Rules Engine"

At first blush, the Computation Engine project is just what Martin Fowler describes: a way of specifying a sequence of "rules" to execute. As with many rules engines, you specify formulas in a distinct expression language, which can be read in to your application at runtime.

However, Computation Engine's computations really aren't rules in the technical sense. The rules in a rules engine are generally if-then statements. They can be written in any order, and more than one of them may apply in a given situation. For example: "If a person is over 65, the person gets a senior discount. If a person is handicapped, the person can park in a handicapped spot."

Computations, on the other hand, don't need to be in if-then form. Sequence may matter. For example: "To calculate the total, take the quantity of each item and multiply it by the unit price. Then take the sum of all the results--the subtotal. Then, take the quantity of each taxable item and multiply it by the unit price, take the sum of those results, and multiply by the tax rate. Add that result to the subtotal."

In other words, put in some input, run through a deterministic series of steps, and get some output.

Why a Computation Engine, then? Why not just specify the series of steps in your code?

Having your business rules specified in one place and not hard-coded into your application can be helpful when they are volatile and complex. The rules are kept separate from the structure of your application, from other concerns your object model might address, and from other reasons those objects might change. Changing a calculation means changing one thing in one place. And you don't have to rebuild and redeploy a service (or your entire system) whenever your business rules change.

Computation Engine's "sweet spot" differs from that of rules engines in that it is more appropriate for algorithms that involve a deterministic sequence of steps, rather than if-then rules that may be applied in any order. Unlike some rules engines that seem forbidding, Computation Engine is also fairly straightforward to use, while still having a powerful expression language: Scala.

Computation Engine is a Scala library, and is intended to be used in Scala applications. Although computations are written in Scala as well, they aren't compiled in to your application; they get evaluated (in a sandbox) by the Scala Script Engine at runtime. As a result, you can do things like store computations as text strings in a database and read them in when your application starts.

But don't go thinking that Computation Engine is designed to let business people specify computations themselves, as some rules engines claim to allow. Using this library requires not only knowing how to write Scala code to express computations, but also how to instantiate computations from your program. A user will need to have a fair amount of tech-savvy to use this library. Computation Engine is not a Business-Writable DSL, nor even a Business-Readable DSL.

Computation Engine has significant implications on how you write your code. The beginning of the chain of computation receives a Scala map of Symbol -> Any, and the ultimate result is a similar map. Abandon all type safety ye who enter here!

The advantage of removing type constraints is that it allows computations to be specified on any arbitrary subset of the incoming data. For any given computation, you use particular values from the data map with certain types, e.g., the computation may use the value for the key 'maxValue, which is an integer, while also using that for 'accountId, which is a string. But the incoming data map can combine values of many different types, and the results of computations can add values of new types to the map, without facing the wrath of the type checker. (This may be what Rich Hickey means when he says "let data be data.")

Of course, once the chain of computations is over and you go back into Scala land, you'll have to return data of types that your application expects. But how you get there is something you can change on the fly without having to modify the rest of your application.

Since you can't rely on the compiler to check that the right data is being passed in, you'll have to test the hell out of your computations' clients to make sure they will always obey the contract your computation implicitly specifies. Similarly, you need to test each computation in the chain to make sure its results will always follow the contracts of later computations that depend on that data. Finally, you need to test that you will always be providing results in a form that client code expects. One future direction for Computation Engine is to supply an application that will infer these contracts and automatically specify what tests need to be written. Ideally, ScalaCheck could be used to automate some of these tests.

Philosophically speaking, Computation Engine is not really in harmony with object-oriented design. An object-oriented system generally groups different kinds of data with the operations that pertain to that data; business rules are expressed in the form of interactions among groups of objects. As Steve Freeman and Nat Pryce put it in GOOS, "The behavior of the system is an emergent property of the composition of the objects--the choice of objects and how they are connected."

The result of object-oriented design is a system in which business rules are "coded in." Depending on the system, it may be easier or harder to change coded-in rules: A static, compiled, class-based language makes them difficult to change, while a dynamic, interpreted, object-based language may make it fairly easy to do so. But the rules are not collected together in one place; they emerge from the interaction of the entire system of objects. Tugging on something over here can result in unexpected consequences over there.

Computation Engine instead envisions a strict separation of operations and data. This means that once the computations are finished, you'll likely be putting the results into objects that are little more than data structures, without much in the way of methods or behaviors. In other words, you'll have an Anemic Domain Model. This is by design, and you'll have to put up with being accused of promoting an anti-pattern. Situations in which your business rules are complex and likely to fluctuate a lot are not ideal candidates for object-oriented design: You'll spend a lot of time coming up with careful abstractions to cover all the scenarios you know about, and then something will come along that overturns some fundamental assumption and force you to redesign the whole thing.

The approach taken by Computation Engine is not entirely flexible, of course. You can modify your computations as you wish, but your flexibility is still limited by the data you pass in. If your database queries or user input fields don't supply the data you need, then you'll have no alternative but to re-code them to provide that data. The runtime specification of arbitrary database queries or user input fields is beyond the scope of this framework, and may not even be desirable from a data security point of view. But you'll be a lot more likely to succeed at predicting all the possible data that might be relevant to a certain type of computation, than at predicting all the different possible ways in which the computation might need to be carried out.

Computation Engine is friendlier to functional programming than to OO. Computations are intended to be stateless; the default security configuration for the Scala Script Engine generally restricts the accessible classes to ones that have no side effects. You can get around this restriction by supplying your own security configuration, but it's not recommended. Testing computations is hard enough without having to worry about verifying changes in state.

Besides tools for testing, the roadmap for Computation Engine envisions providing a library for reading rules from persistent storage, as well as editing tools for authoring, persisting, and publishing rules. Stay tuned for developments.

In the meantime, enjoy!