[Elixir ORM #2] `~>` 和 `<~` 的实现
首先列一下需求吧:
- C/S 模式,需要在当前 node 调用,同时需要在其它 node 通过
:rpc.call
调用 - 在服务端定义的
User
等 module 对应的表,不需要在每个 client 重新定义 - 不希望占用
where
order
insert
update
等常用函数 - 不喜欢 LINQ
所以使用 ~>
和<~
只是因为我实再找不到其它的办法能满足上面的需求。
实现过程中,第一步肯定是调研。我先测试了一下 quote do: User ~> where(id: 1)
,结果是{:~>, [], [{:__aliases__, [alias: false], [:User]}, {:where, [], [[id: 1]]}]}
这说明现 ~> 的优先级还是比较高的,有足够的发挥空间,这么实现是可行的。
接下来是实现,<~ 是相对简单的,因为 <~ 不会嵌套,所以可以很容易地如下实现:
defmacro left <~ {:ins, _, [arg]} do quote do Ench.Action.insert(unquote(left), unquote(arg)) end end defmacro left <~ {:del, _, nil} do quote do Ench.Action.delete(unquote(left)) end end defmacro left <~ {:set, _, [arg]} do quote do Ench.Action.update(unquote(left), unquote(arg), %{}) end end defmacro left <~ {:inc, _, [arg]} do quote do Ench.Action.update(unquote(left), %{}, unquote(arg)) end end defmacro left <~ {:update, _, [set, inc]} do quote do Ench.Action.update(unquote(left), unquote(set), unquote(inc)) end end
而 ~> 是多级的,quote 之后是这个样子的( [10]
在打印的时候会显示为 '\n'
)
iex(1)> quote do: User ~> limit(10) ~> all {:~>, [], [{:~>, [], [{:__aliases__, [alias: false], [:User]}, {:limit, [], '\n'}]}, {:all, [], Elixir}]}
这里我最大的疑惑就是「这段递归要在编译的时候做还是在运行的时候做」,于是赶紧去看下 elixir 里|>
的实现,找到了这段代码 https://github.com/elixir-lang/elixir/blob/v1.1/lib/elixir/lib/macro.ex#L66-L115,这段代码给了一个明确的答案,应该在编译的时候做,就是说,在代码里写的 f(g(a))
和 a|> g |> f
在编译之后,是完全一样的。
defmacro left ~> right do [h | t] = unpipe({:~>, [], [left, right]}) List.foldl(t, h, fn(expr, acc) -> pipe(acc, expr) end) end defp unpipe(expr) do unpipe(expr, []) |> Enum.reverse end defp unpipe({:~>, _, [left, right]}, acc) do unpipe(right, unpipe(left, acc)) end defp unpipe(other, acc) do [other | acc] end defp pipe({:__aliases__, _, m}, {query, line, args}) do module = Module.concat(m) query_struct = Ench.Query.init(module) |> Macro.escape call = {:., [], [{:__aliases__, [alias: false], [:Ench, :Query]}, query]} {call, line, [query_struct | args || []]} end defp pipe(left, {call, line, args}) do {pipe_call, _, _} = quote do: Ench.DSL.do_pipe {pipe_call, line, [left, call, args]} end def do_pipe(%Ench.Query{}=query, call, args) when call in [ :where, :offset, :limit, :order, :first, :all, :count, :find, :find!, :find_by, :find_by!, ] do apply(Ench.Query, call, [query | args || []]) end def do_pipe(module, call, args) when is_atom(module) do apply(Ench.Query, call, [Ench.Query.init(module) | args || []]) end
代码长得很像 Macro 里的 pipe 和 unpipe,只是针对需求改了下。
这样基本上,ORM 的部分就可以完结了,再详细看下其它部分的实现,有没有值得写的,然后,会再写一个「总结」。所以,待续。