Convert boolean expression into an anonymous function

I’m wondering if the following is possible:

I have an application where I would like to parse boolean expressions and produce corresponding anonymous functions. These expressions:

x > y
x == y
x == y + 3

would be converted to:

fn data -> data.x > data.y
fn data -> data.x == data.y
fn data -> data.x == data.y + 3

Possible?

Hey @CharlesIrvine can you elaborate if you are being passed these expressions as strings from users or other untrusted sources at runtime, vs compile time expressions?

1 Like

Ben beat me to the question so I’ll just add that if you simply want to convert an existing program and NOT do it at runtime while accepting user input then using a tool like GitHub - ast-grep/ast-grep: ⚡A CLI tool for code structural search, lint and rewriting. Written in Rust that parses code properly and can replace it should be more than enough.

@benwilson512 @dimitarvp I should have provided more context. Sorry.

I currently have these functions and macro:

def x_less_than_y(data) do
    data.x < data.y
  end

  def x_greater_or_equal_y(data) do
    data.x >= data.y
  end

defprocess "two case process" do
    case_task("yes or no", [
      case_i &ME.x_less_than_y/1 do
        user_task("1", groups: "admin")
        user_task("2", groups: "admin")
      end,
      case_i &ME.x_greater_or_equal_y/1 do
        user_task("3", groups: "admin")
        user_task("4", groups: "admin")
      end
    ])
  end

From this file.

I would be really useful if the users of my BPM library could write this instead:

defprocess "two case process" do
    case_task("yes or no", [
      case_i x < y do
        user_task("1", groups: "admin")
        user_task("2", groups: "admin")
      end,
      case_i x >= y do
        user_task("3", groups: "admin")
        user_task("4", groups: "admin")
      end
    ])
  end

This is my first foray into writing Elixir macros. I thought there might be some macro tricks that could be used for this.

There are useful shorthands like quote, but ultimately macros are transformations from AST to AST. For your example:

# INPUT
iex(1)> quote do: x > y 
{
  :>,
  [context: Elixir, import: Kernel],
  [
    {:x, [if_undefined: :apply], Elixir},
    {:y, [if_undefined: :apply], Elixir}
  ]
}
# OUTPUT
iex(2)> quote do: data.x > data.y
{
  :>,
  [context: Elixir, import: Kernel],
  [
    {
      {
        :.,
        [],
        [
          {:data, [if_undefined: :apply], Elixir},
          :x
        ]
      },
      [no_parens: true],
      []
    },
    {
      {
       :.,
       [],
       [
         {:data, [if_undefined: :apply], Elixir},
         :y
       ]
     },
     [no_parens: true],
    []
   }
 ]
}

The underlying pattern looks like:

{:some_var, _, _} -> {{:., [], {:data, [], Elixir}, :some_var}

You’d apply a transformation like this using a function like Macro.prewalk or your own recursive traversal code.

Once you’ve got the data-fied expression, you’d wrap it in the AST corresponding to fn data -> ... end. You can find that shape using quote:

iex(4)> quote do: fn data -> :ok end
{:fn, [], [{:->, [], [[{:data, [if_undefined: :apply], Elixir}], :ok]}]}