ETS - conditional match with variable in Elixir

I want to query an ETS table based on a condition. As per Erlang’s doc, I should be able to use variables in scope.
Is it possible to formulate the query below with this “imported variable” in Elixir ?

def get(id) do
  fun = :ets.fun2ms(fn {x,y,z,:_} when x == id or y == id -> {x,y,z} end)
  :ets.select(:table, fun)
end

This does not work with a variable, but works with a value.

(FunctionClauseError) no function clause matching in :ms_transform.normalise/1

You can obtain the result as below but not satisfied

def get(id), do:
[
    :ets.match_object(:table, {:"$1", id, :"$2", :_})
      | :ets.match_object(:table, {:"$1", :"$2", id, :_})
]
|> List.flatten()

I’m not sure why exactly, but :ets.fun2ms doesn’t work in elixir (or at least not in all contexts). There’s :ex2ms to solve this:

import Ex2ms

fun do {x,y,z,_} when x == 1 or y == 1 -> {x,y,z} end
# [
#   {{:"$1", :"$2", :"$3", :_}, [{:orelse, {:==, :"$1", 1}, {:==, :"$2", 1}}],
#    [{{:"$1", :"$2", :"$3"}}]}
# ]

Replace 1 with id.

Yes this produces the “desired” :slight_smile: pattern. Thks

q = [{
  {:"$1", :"$2", :"$3", :_},
  [{:orelse, {:==, :"$2", {:const, id}}, {:==, :"$3", {:const, id}}}],
  [{{:"$1", :"$2", :"$3"}}]
}]

with:

q =
   Ex2ms.fun do
        {ch, e, r, _} when e == ^id or r == ^id -> {ch, e, r}
   end

Hmmm, the full stack trace is interesting:

iex(7)> :ets.fun2ms(fn {x,y,z,:_} when x == id or y == id -> {x,y,z} end)   
** (FunctionClauseError) no function clause matching in :ms_transform.normalise/1    
    
    The following arguments were given to :ms_transform.normalise/1:
    
        # 1
        {:EXIT,
         {:badarg,
          [
            {:lists, :keysearch, [:_@0, 1, %{_@0: 4}],
             [error_info: %{module: :erl_stdlib_errors}]},
            {:ms_transform, :fixup_environment, 2,
             [file: 'ms_transform.erl', line: 1104]},
            {:lists, :map_1, 2, [file: 'lists.erl', line: 1320]},
            {:lists, :map, 2, [file: 'lists.erl', line: 1315]},
            {:lists, :map_1, 2, [file: 'lists.erl', line: 1320]},
            {:lists, :map_1, 2, [file: 'lists.erl', line: 1320]},
            {:lists, :map, 2, [file: 'lists.erl', line: 1315]},
            {:ms_transform, :fixup_environment, 2,
             [file: 'ms_transform.erl', line: 1112]}
          ]}}
    
    (stdlib 4.1.1) ms_transform.erl:1124: :ms_transform.normalise/1
    (stdlib 4.1.1) ms_transform.erl:222: :ms_transform.transform_from_shell/3
    (stdlib 4.1.1) ets.erl:605: :ets.fun2ms/1
    iex:7: (file)

There’s actually TWO crashes happening here:

  • :ms_transform.normalize is being called with an {:EXIT, ...etc} tuple because the error handling in transform_from_shell wasn’t expecting that shape:
  • that tuple is coming from :ms_transform.fixup_environment, which expects an environment B as a keyword list but gets a map instead:

The root cause is that :erl_eval.fun_data/1 is returning a map for the bound variables :thinking: :

iex(9)> :erl_eval.fun_data(fn {x,y,z,:_} when x == id or y == id -> {x,y,z} end)
{:fun_data, %{_@0: 4},
 [
   {:clause, 9,
    [
      {:tuple, 9,
       [{:var, 9, :_x@1}, {:var, 9, :_y@1}, {:var, 9, :_z@1}, {:atom, 9, :_}]}
    ],
    [
      [
        {:op, 9, :orelse, {:op, 9, :==, {:var, 9, :_x@1}, {:var, 9, :_@0}},
         {:op, 9, :==, {:var, 9, :_y@1}, {:var, 9, :_@0}}}
      ]
    ], [{:tuple, 9, [{:var, 9, :_x@1}, {:var, 9, :_y@1}, {:var, 9, :_z@1}]}]}
 ]}
1 Like

fun2ms and friends are parse transformations, they can be used from the shell, but the transformation is applied at compile time using ms_transform.hrl which cannot really be done in elixir. See the full documentation especially the warnings at the end of the documentation.