Skip to content

Instantly share code, notes, and snippets.

@princejwesley
Last active October 7, 2020 17:24
Show Gist options
  • Save princejwesley/bc4b164f277b331916da106840b80990 to your computer and use it in GitHub Desktop.
Save princejwesley/bc4b164f277b331916da106840b80990 to your computer and use it in GitHub Desktop.
Minimal LINQ like Query for elixir
defmodule Enum.Query.CompileError do
@moduledoc ~S"""
Raise compile time error while building query
"""
defexception [:message]
end
defmodule Enum.Query do
@moduledoc ~S"""
Query builder of below form:
from x in xs,
:where x > 100
from x in xs,
:where x > 100,
:where div(x, 4),
:select x + 2
from {x,y} in xys,
:where x in ["yes", "no"],
:select y
"""
# Query clauses
@clauses [:where, :select]
@doc ~S"""
build from query
"""
defmacro from(expr, kws \\ []) do
# second argument must be keyword lists
unless Keyword.keyword?(kws), do: raise ArgumentError, "second argument must be a keyword list"
from!(expr, kws, nil)
end
defp from!({:in, _, [vars, rvalue]} = expr, [{clause, action} | t], output) when clause in @clauses do
query = if output, do: output, else: rvalue
from!(expr, t, build_clause!(vars, query, clause, action))
end
defp from!({:in, _, _}, [], output) do
output
end
defp from!(_, _, _) do
#support only `from var in enumerables` form
raise ArgumentError, "first argument must be of form 'from var in enumerables'"
end
defp extract_vars(vars) do
case vars do
{v, _, _} -> Atom.to_string(v)
v when is_tuple(v) -> Tuple.to_list(v) |> Enum.map(&extract_vars/1)
end
end
defp vars!(vars) do
v = List.wrap(vars) |> Enum.map(&extract_vars/1)
dups = v -- Enum.uniq(v)
unless dups == [], do: query_error("`#{hd dups}` is bound more than one time")
nil
end
defp build_clause!(vars, rvalue, :where, action) do
quote do
Enum.filter(unquote(rvalue), fn (unquote(vars)) -> unquote(action) end)
end
end
defp build_clause!(vars, rvalue, :select, action) do
quote do
Enum.map(unquote(rvalue), fn (unquote(vars)) -> unquote(action) end)
end
end
# drop frames of ours
defp query_error(msg) do
# old = :erlang.system_flag(:backtrace_depth, 20)
#ignore Process.info stack trace
{:current_stacktrace, [_|st]} = Process.info(self, :current_stacktrace)
st = Enum.drop_while st, fn {name, _, _, _} -> String.starts_with?(Atom.to_string(name), ["Elixir.Enum.Query", "elixir_"]) end
# old = :erlang.system_flag(:backtrace_depth, old)
reraise %Enum.Query.CompileError{message: msg}, st
end
end
@princejwesley
Copy link
Author

Simple enumerable LINQ like macro with where and select clauses.

require Enum.Query
import Enum.Query

xs = [2, 4, 6, 1, 3, 16, 20, 100, 70]
ys = from x in xs,
      :where x > 100,
      :where div(x, 4),
      :select x + 2
IO.inspect ys

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment