Dantangfan

Elixir 宏里使用 bind_quoted 的坑

今天看 sf 的时候逛到这么个问题 stackoverflow,想起自己以前写宏代码生成的时候也遇到过奇葩的问题,具体怎样记不清了,或多或少跟这个有些相似。于是,我也很不建议在代码中随意使用 bind_quoted 操作。

bind_quoted 实际上做的工作很简单,比如代码

quote bind_quoted: [foo: foo] do
  IO.inspect foo
end

就直接搞成了

quote do
  foo = unquote(foo)
  IO.inspect foo
end

但是如果 foo 是个表达式的话,就会在这一步直接执行了,所以才会有你上面写的 Macro.escape,让表达式在bind_quoted中不执行。

那么,如果我有以下代码要生成

defmodule Q do
  defmacro gen_func([do: code]) do
    quote do
      def new_fun() do
        unquote(code)
      end
    end
  end
end
defmodule T do
  require Q
  Q.gen_func do
    IO.inspect "hahahah"
  end
end

这个时候,需要 unquote 的是一段代码,于是就不能简单 bind_quoted, 一个可能的姿势是这样

  defmacro gen_func([do: code]) do
    code = Macre.escape(code)
    quote bind_quoted: [code: code] do
      def new_fun() do
        unquote(code)
      end
    end
  end

但是我认为这样不太好,因为代码中的 code 是从 do: code, 中取出来的,此时的 code 已经是一个合法的 AST。经过 escape 然后 bind_quoted ,实际上什么都没变,所以如果要在 quote 中生成的函数里使用 code 代码,实际上还是需要再 unquote 一次,因为 def 生成函数的过程中,又进入了一块新的 AST,code 数据对他来说又是外部数据,所以也必须使用 unquote。

其次, bind_quoted 有个规则是默认关闭在 quote 中使用 unquote,除非设置 unquote: true。如果不加这个选项,就要一次性 unquote 出来所有需要的变量数据,然而混合使用感觉更不爽,特别是需要 unquote 数据比较多的时候,一些提前 unquote 了,一些又在中途 unquote,感觉更是奇怪。

大量的数据一次行就 bind_quoted 了,而不是在使用的时候再 unquote,还可能造成调试时候的 tracback 位置不对。

再次,bind_quoted 之后,quote 代码的中间产生的值不能对那些名字进行再绑定,原因不用解释。

而且,bind_quoted 还有些奇怪的行为。比如

    defmodule Q do
      defmacro __using__([name: name]) do
        quote bind_quoted: [fun_name: name] do
          a = 1
          def fun_name do
            IO.inspect unquote(name)
          end
        end
      end
      defmacro mod(name) do
        quote bind_quoted: [name: name] do
          defmodule name do
            IO.inspect __MODULE__
          end
        end
      end
    end
    defmodule T do
      use Q, name: :fffffffffff
      require Q
      Q.mod Test
    end

上面的代码,生成 function 的bind_quoted没有生效,而生成module的bind_quoted 生效了的。

暂时就想到这么多




blog comments powered by Disqus