tadasv site

Making s-expression evaluator

For the past few weeks I’ve been working on a new personal project. I want to build an experimentation platform that lets you easily setup and run various experiments on the web. One key component of this project is to be able to encode experiment configuration as data instead of implementing all the rules in code. Having configuration in data makes it easier to change and update experiments on the fly without having to rebuild the app.

I started developing Brogue parser and evaluator. Brogue as in accent, not the shoe. Brogue expression syntax is based on the idea of Lisp’s S-Expressions. However, it’s encoded as JSON object (so it can be more easily parsed and understood by the frontend code if needed). Let’s look at this example:

{
  "if": {
    {
      "lt": [
        {"hashmod": "user-1", 100},
        10
      ]
    },
    "Sign up for 14-day trial",
    "Start free trial now"
  }
}

Example above is a valid Brogue expression. Once evaluated it will return either “Sign up for 14-day trial” or “Start free trial now”. The example above uses 3 functions: if, lt and hashmod. In this example, hashmod takes a hash of value “user-1” and mods it by 100. Then passes it to the lt function which checks if the mod is less than 10 or not. If it is smaller than 10, we will return 14-day trial message, otherwise we will get “Start free trial now” back.

Where this gets really interesting is that you can actually supply external information when evaluating the expression.

context := json.RawMessage(`{"user_id": "user-1"}`)

evaluator.Evaluate(context, `
{
  "if": {
    {
      "lt": [
        {"hashmod": {"context": ["user_id"], 100},
        10
      ]
    },
    "Sign up for 14-day trial",
    "Start free trial now"
  }
}
`)

This example is actually equivalent to the first one. It will produce the same result. However, now the user id is passed from some external source instead of being hardcoded in the rules. This open up lots possibilities for constructing complex rules that produce different results once the operating context changes. Brogue is also extensible. I am able to add new expressions very easily by implementing a Go function and binding it to some function name such as “and”.

At the moment Brogue is implemented in Go and is still in the works. I may open it up once it’s complete and I have more concrete cases.