Home navigation headshot

🍱 PortableExpressions

⌨️ Ruby 👾 Source →

TL;DR — A simple and flexible pure Ruby library for building and evaluating expressions. Fully compatible with JSON.

I wrote PortableExpressions because I needed a way to write math expressions that were fully JSON serializable for another project. Although that project got sidetracked, I decided to release the code for building and serializing these expressions as a library.

Because most (all?) math operators in Ruby are implemented as methods, what I really ended up building was a framework to express any procedure or function call. It works great for math, but can do anything that you can describe as a reduction of an array by some method. The elements in the array are the operands, and the method is the operator. Here, reduction just means:

elementN.operator(elementN+1)
        .operator(elementN+2)
        .operator(elementN+3)...

…and is implemented using Ruby’s reduce (aka inject).

# Addition
[1, 2].reduce(:+) #=> 3

# is equivalent to
1.+(2) #=> 3

# Multiplication
[2, 2, 2].reduce(:*) #=> 8

# is equivalent to
2.*(2)
 .*(2) #=> 8

The library is made up of 4 main objects, all of which are fully JSON serializable (and deserializable):

  1. Scalars, which represent an “atom” or value that can be operated on. A Scalar’s value can be any JSON type.
  2. Variables, which represent a named value stored in the Environment (more on this later). These allow you to defer evaluation of some object until the Variable is used.
  3. Expressions, which represent an array of operands and an operator. The operands can be Scalars, Variables, or even other Expressions. The operator can be any Ruby symbol representing a method that all but the last operands must support.
  4. Environments, which represent the state of the procedure and hold the values that Variables “point” to.

Expressions are stateless by default and can be evaluated by any Environment. A procedure can be described by one or more Expressions once and can be used multiple times. By allowing Expressions to be built independently of the Environment, the library allows your system to get instructions (Expressions) from one source, and the input (Environment) from elsewhere.

One example of this abstraction in practice is implementing authorization policies via stateless Expressions, and then executing them on various distributed services using an Environment that contains the relevant, local context. Here, each service might use one or some combination of policies and inject the relevant user and other info through the Environment. The environment itself can be serialized so you can invert the architecture by having a centralized authorization service supports many policies, and various distributed services that request decisions through some API. The POST body might contain the serialized Environment along with info about which policy the services is requesting execution for.

In order to support the logical operators, which are not methods in Ruby, I implemented special methods on a SimpleDelegator which wraps operands when evaluating an Expression. This allowed me to extend the functionality of each operand class without polluting the actual model. The delegator currently implements :and (&&) and :or (||) to support logical Expressions.


, fork, or forget. Copyright © 2024 Omkar Moghe.