Project

conflisp

0.0
Low commit activity in last 3 years
No release in over a year
A tool for building JSON-serializable lisps.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Development

~> 0.93.1
~> 0.19.0
~> 3.12.0
 Project Readme

Gem Version

Conflisp

A tool for buliding JSON-serializable lisps.

Sometimes you need to make something configurable at the database level, but you don't want your application to have to support all possible variations of that configuration.

You can also use Conflisp as a sort of JSON template language, à la Jsonnet.

Installation

RubyGems

gem install conflisp

Bundler

gem 'conflisp'

Usage

  1. Define your functions

    MyLang = Conflisp::Language.define do
      fn :add, ->(a, b) { a + b }
      fn :subtract, ->(a, b) { a - b }
    end
  2. Use it

    > MyLang.evaluate(['add', 2, ['subtract', 3, 1]])
    => 4

You can pass in variables into the evaluator and you will have access to them in your expressions:

> MyLang.evaluate(['add', 2, ['global', 'foo']], globals: { 'foo' => 3 })
=> 5

Normally you would store the expressions in a database and then evaluate them when you need to:

> myfoo = MyFoo.find(123)
> configuration = MyLang.evaluate(myfoo.config, globals: { 'current_time' => Time.now.utc })

Your functions can also refer to other functions using the resolve method:

MyLang = Conflisp::Language.define do
  fn :add, ->(a, b) { a + b }

  fn :add_twice, ->(a, b) do
    resolve([
      'add',
      ['add', a, b],
      ['add', a, b]
    ])
  end
end

Conflisp only provides one built-in function (global). We don't want to be opinionated on the ordering of the arguments to your functions (think lodash vs. lodash/fp), so it is up to the application developer to implement all of their functions.

You can also extend your languages to add new functions:

BaseLang = Conflisp::Language.define do
  fn :add, ->(a, b) { a + b }
end

# NewLang will also have access to the `add` method.
NewLang = BaseLang.extend do
  fn :foo, ->() do
    # ...
  end
end

Example language

MyLang = Conflisp::Language.define do
  # Without this we wouldn't be able to express arrays
  fn :list, ->(*values) do
    values
  end

  fn :'==', ->(left, right) do
    left == right
  end

  fn :'!=', ->(left, right) do
    left != right
  end

  # Boolean

  fn :and, ->(*values) do
    raise ArgumentError, 'and requires at least 2 arguments' if values.size < 2

    values.reduce do |prev, value|
      prev && value
    end
  end

  fn :or, ->(*values) do
    raise ArgumentError, 'or requires at least 2 arguments' if values.size < 2

    values.reduce do |prev, value|
      prev || value
    end
  end

  # Mathematics

  fn :'<=', ->(left, right) do
    left <= right
  end

  fn :'>=', ->(left, right) do
    left >= right
  end

  # Collections

  fn :dig, ->(collection, *paths) do
    collection.dig(*paths)
  end

  # Special

  # This just a wrapper that gives us access to some config passed in through globals
  fn :config, ->(*keys) do
    resolve(['global', 'config']).dig(*keys)
  end
end

Then you could run a complex expression like this:

> MyLang.evalute(
    [
      'join',
      ' ',
      [
        'or',
        ['and', ['>=', ['config', 'current_hour'], 19], 'Good evening'],
        ['and', ['>=', ['config', 'current_hour'], 13], 'Howdy'],
        ['and', ['>=', ['config', 'current_hour'], 11], 'Happy noon'],
        ['and', ['>=', ['config', 'current_hour'], 9], "G'day"],
        'Hello'
      ],
      ['config', 'user', 'name']
    ]
    globals: {
      'config' => {
        'current_hour' => Time.now.hour, # 14
        'user' => {
          'name' => 'Billy'
        }
      }
    }
  )
=> "Howdy Billy"

Errors

When evaluating Conflisp expressions, there are two types of errors that can be raised:

  • Conflisp::MethodMissing - raised if a method isn't defined in the language
  • Conflisp::RuntimeError - raised if there was an error during evaluation

License

MIT License

Copyright © 2020 Everwise