16、Elixir宏:上下文、卫生性及应用实践

Elixir宏:上下文、卫生性及应用实践

1. 上下文与宏卫生性

宏在注入代码时,需要特别关注调用者上下文和宏自身上下文。宏注入的代码不能默认某些变量可用。例如,以下宏定义尝试访问调用者上下文的变量:

defmodule ContextInfo do
  defmacro grab_caller_context do
    quote do
      IO.puts x
    end
  end
end

iex 中加载该模块并调用宏,会得到编译错误:

iex(1)> c "context.exs"
[ContextInfo]
iex(2)> import ContextInfo
nil
iex(3)> x = 42
42
iex(4)> grab_caller_context
** (CompileError) iex:4: undefined function x/0
    expanding macro: ContextInfo.grab_caller_context/0
    iex:4: (file)

同样,宏不能安全地注入改变调用者上下文或环境的代码。例如,向 ContextInfo 模块添加一个尝试改变 x 值的宏:

defmacro inject_context_change do
  quote do
    x = 0
    IO.puts x
  end
end

调用该宏后,调用者上下文的 x 值并未改变:

iex(1)> c "context.exs"
[ContectInfo]
iex(2)> import ContextInfo
nil
iex(3)> x = 42
42
iex(4)> inject_context_change
0
:ok
iex(5)> x
42

下面两个模块更明确地展示了宏上下文与调用者上下文的区别:

defmodule MacroContext do
  defmacro info do
    IO.puts "Macro context: (#{__MODULE__})"
    quote do
      IO.puts "Caller context: (#{__MODULE__})"
      def some_info do
        IO.puts """
        I am #{__MODULE__} and I come with the following:
        #{inspect __info__(:functions)}
        """
      end
    end
  end
end
defmodule MyModule do
  require MacroContext
  MacroContext.info
end

iex 中加载这两个模块,输出如下:

iex(1)> c "context2.exs"
Macro context: (Elixir.MacroContext)
Caller context: (Elixir.MyModule)
[MyModule, MacroContext]
iex(2)> MyModule.some_info
I am Elixir.MyModule and I come with the following:
[some_info: 0]
:ok

__MODULE__ 宏能为每个上下文显示正确的模块名,这是不卫生宏的结果,下面将详细介绍。

2. 不卫生宏:覆盖上下文

编写好的宏时,有时需要覆盖上下文,类似于处理编程中的状态能产生有用的程序。Elixir提供了 var! 宏来实现这一点。 var! 宏非常通用,它允许我们在不将调用者上下文的成员传递给宏的情况下访问它们,并允许宏修改调用者的上下文。
修改之前的例子,添加 var!

defmodule ContextInfo do
  defmacro grab_caller_context do
    quote do
      IO.puts var!(x)
    end
  end
end

iex 中加载并编译更新后的 ContextInfo 模块,宏可以访问调用者上下文的 x 变量:

iex(1)> c "context.exs"
[ContextInfo]
iex(2)> require ContextInfo
nil
iex(3)> x = 42
42
iex(4)> ContextInfo.grab_caller_context
42
:ok

同样,更新 inject_context_change 宏:

defmodule ContextInfo do
  defmacro inject_context_change do
    quote do
      var!(x) = 0
    end
  end
end

iex 中加载更新后的模块,宏可以改变调用者上下文的 x 变量的值:

iex(1)> c "context.exs"
[ContextInfo]
iex(2)> require ContextInfo
nil
iex(3)> x = 42
42
iex(4)> ContextInfo.inject_context_change
0
iex(5)> x
0

使用 var! 宏允许宏从调用者上下文提取和注入值,实现不卫生操作。但需要注意,这是非函数式的,会增加宏的复杂性,使用时需谨慎权衡。

3. 宏的实际应用示例
3.1 调试和跟踪

我们可以创建一个调试/跟踪模块,自动跟踪库中方法的调用。首先,了解 use __using__ use 是一个相对简单的函数,它调用传递模块的 __using__ 宏,进而注入 __using__ 宏的代码。
例如,定义一个基本模块 UsingTest

defmodule UsingTest do
  defmacro __using__(_opts) do
    quote do
      IO.puts "I'm the __using__/1 of #{unquote(__MODULE__)}"
    end
  end
end

再定义一个简单模块 MyUsingTest

defmodule MyUsingTest do
  use UsingTest
end

编译这两个模块,会输出:

iex(1)> c "usingtest.exs"
I'm the __using__ of Elixir.UsingTest
[TestMyUsing, UsingTest]

为了给库中的函数调用添加跟踪信息,我们需要重新定义 def 宏。创建一个 Tracer 模块:

defmodule Tracer do
  defmacro def(definition={name, _, args}, do: content) do
    quote do
      Kernel.def(unquote(definition)) do
        unquote(content)
      end
    end
  end
  defmacro __using__(_) do
    quote do
      import Kernel, except: [def: 2]
      import unquote(__MODULE__), only: [def: 2]
    end
  end
end

目前,这个新的 def 宏只是增加了函数定义的间接层次,还没有实现跟踪功能。我们添加一个辅助函数 dump_args 来打印函数参数:

def dump_args(args) do
  args |> Enum.map(&inspect/1) |> Enum.join(", ")
end

然后修改 def 宏,使用 dump_args 函数并打印函数名、参数和结果:

defmodule Tracer do
  def dump_args(args) do
    args |> Enum.map(&inspect/1) |> Enum.join(", ")
  end
  defmacro def(definition={name, _, args}, do: content) do
    quote do
      Kernel.def(unquote(definition)) do
        IO.puts :stderr,
                ">>> Calling #{unquote(name)} with #{Tracer.dump_args(unquote(args))}"
        result = unquote(content)
        IO.puts :stderr, "<<< Result: #{Macro.to_string result}"
        result
      end
    end
  end
  defmacro __using__(_) do
    quote do
      import Kernel, except: [def: 2]
      import unquote(__MODULE__), only: [def: 2]
    end
  end
end

创建一个简单的排序模块 Quicksort ,并使用 Tracer 模块:

defmodule Quicksort do
  use Tracer
  def sort(list), do: _sort(list)
  defp _sort([]), do: []
  defp _sort(l = [h|_]) do
    (l |> Enum.filter(&(&1 < h)) |> _sort)
    ++ [h] ++
    (l |> Enum.filter(&(&1 > h)) |> _sort)
  end
end

iex 中加载并调用 sort 函数,会输出跟踪信息:

iex(1)> import Quicksort
nil
iex(2)> 1..10 |> Enum.reverse |> Quicksort.sort
>>> Calling sort with [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
<<< Result: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
3.2 静态数据转换为函数

由于宏在编译时展开,我们可以在注入代码时做一些有趣的事情,例如从静态数据创建函数。Elixir使用文本文件中的Unicode代码点来创建与 String.upcase/1 String.downcase/1 等相关的函数,以支持完整的Unicode。
以下是 String.Unicode 模块中 String.downcase/1 函数的实现:

data_path = Path.join(__DIR__, "UnicodeData.txt")
{codes, whitespace} = Enum.reduce File.stream!(data_path), {[], []}, 
fn(line, {cacc, wacc}) ->
  [codepoint, _name, _category,
   _class, bidi, _decomposition,
   _numeric_1, _numeric_2, _numeric_3,
   _bidi_mirror, _unicode_1, _iso,
   upper, lower, title] = :binary.split(line, ";", [:global])
  title = :binary.part(title, 0, byte_size(title) - 1)
  cond do
    upper != "" or lower != "" or title != "" ->
      {[{to_binary.(codepoint),
         to_binary.(upper),
         to_binary.(lower),
         to_binary.(title)} | cacc],
       wacc}
    bidi in ["B", "S", "WS"] ->
      {cacc, [to_binary.(codepoint) | wacc]}
    true ->
      {cacc, wacc}
  end
end

def downcase(string), do: downcase(string, "")
for {codepoint, _upper, lower, _title} <- codes, lower && lower != codepoint do
  defp downcase(unquote(codepoint) <> rest, acc) do
    downcase(rest, acc <> unquote(lower))
  end
end
defp downcase(<<char, rest :: binary>>, acc) do
  downcase(rest, <<acc :: binary, char>>)
end
defp downcase("", acc), do: acc

这个例子展示了从文件加载静态数据并为模块生成函数的常见模式。

4. 测试宏

测试模块化代码通常比较容易,但测试宏可能有点像测试黑魔法。一般原则是测试生成的代码,而不是生成代码的能力。
例如,对于一个 while 宏,我们可以编写如下测试:

defmodule WhileTest do
  use ExUnit.Case
  test "while loops while truthy" do
    pid = spawn(fn -> :thread.sleep(:infinity) end)
    send self, :one
    while Process.isAlive?(pid) do
      receive do
        :one -> send self, :two
        :two -> send self, :three
        :three ->
          Process.exit(pid)
          send self, :done
       end
    end
    assert_received :done
  end
end

对于标准库中的 String.upcase/1 函数,测试如下:

test "upcase" do
  assert String.upcase("123 abcd 456 efg hij ( %$#) kl mnop @ qrst = -_ uvwxyz") == "123 ABCD 456 EFG HIJ ( %$#) KL MNOP @ QRST = -_ UVWXYZ"
  assert String.upcase("") == ""
  assert String.upcase("abcD") == "ABCD"
end

test "upcase utf8" do
  assert String.upcase("& % # àáâ ãäå 1 2 ç æ") == "& % # ÀÁÂ ÃÄÅ 1 2 Ç Æ"
  assert String.upcase("àáâãäåæçèéêëìíîïðñòóôõöøùúûüýþ") == "ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞ"
end
test "upcase utf8 multibyte" do
  assert String.upcase("straße") == "STRASSE"
  assert String.upcase("áüÈß") == "ÁÜÈSS"
end

测试宏应像测试模块的公共接口一样,测试注入代码生成的函数,将测试代码生成的问题转化为常规模块测试问题。

5. 特定领域语言(DSL)

宏可以创建小型嵌入式语言来解决特定问题。例如,Ecto项目使用Elixir和宏提供了类似SQL的语言来查询数据存储。
原本的SQL查询:

"select * from weather where prcp > 0" |> DB.Connector.query!

可以用Elixir的DSL表达为:

query = from w in Weather,
        where: w.prcp > 0
        select: w

这种方式可以在编译时检查查询,而将SQL查询写成字符串只能在执行时检查。

6. 创建DSL:XML模板

我们可以创建一个简单的DSL来模板化XML模型。

6.1 宏的状态管理

创建用于构建字符串的DSL时,需要一种保存状态的方法。Elixir提供的 Agents 可以轻量级地实现这一点。
以下是使用 Agent 创建键值存储的示例:

iex(1)> {:ok, kv} = Agent.start_link &HashDict.new/0
{:ok, #PID<0.60.0>}
iex(2)> Agent.get(kv, &(&1))
#HashDict<[]>
iex(3)> Agent.update(kv, &(HashDict.put(&1, :a, 2)))
:ok
iex(4)> Agent.get(kv, &(&1))
#HashDict<[a: 2]>
iex(5)> Agent.get(kv, &(&1)) |> HashDict.get :a
2
iex(6)> Agent.get(kv, &(HashDict.get(&1, :a)))
2

将这个示例改写为一个完整的 KV 模块:

defmodule KV do
  def start_link do
    Agent.start_link(&HashDict.new/0, name: __MODULE__)
  end
  def get(key) do
    Agent.get(__MODULE__, &(HashDict.get(&1, key)))
  end
  def put(key, value) do
    Agent.update(__MODULE__, &(HashDict.put(&1, key, value)))
  end
end

iex 中使用该模块:

iex(1)> c "kv_store.exs"
[KV]
iex(2)> KV.start_link
{:ok, #PID<0.60.0>}
iex(3)> KV.put(:a, 2)
:ok
iex(4)> KV.get(:a)
2
6.2 定义XML DSL

XML由以下部分组成:
- 文档的任意元素,有些可能有属性
- 元素可以嵌套
- 元素必须正确关闭和嵌套

我们定义的DSL可能如下:

xml do
  tag :model, name: "my model" do
    for i <- 0..5 do
      tag :attribute do
        text "some value #{i}"
      end
    end
  end
end

首先,我们定义一些辅助函数来存储、更新和检索用于构建输出的缓冲区:

def start_buffer(state), do: Agent.start_link(fn() -> state end)
def stop_buffer(buffer), do: Agent.stop(buffer)
def put_buffer(buffer, content), do: Agent.update(buffer, &[content | &1])
def render(buffer) do
  Agent.get(buffer, &(&1)) |>
  Enum.reverse |>
  Enum.join("")
end

然后定义 xml tag text 宏:

defmacro xml(do: block) do
  quote do
    {:ok, var!(buffer, Xml)} = start_buffer(["<?xml version=\"1.0\"?>"])
    unquote(block)
    result = render(var!(buffer, Xml))
    :ok = stop_buffer(var!(buffer, Xml))
    result
  end
end

defmacro tag(name, do: inner) do
  quote do
    put_buffer var!(buffer, Xml), "<#{unquote(name)}>"
    unquote(inner)
    put_buffer var!(buffer, Xml), "</#{unquote(name)}>"
  end
end
defmacro text(string) do
  quote do: put_buffer(var!(buffer, Xml), to_string(unquote(string)))
end

完整的 Xml 模块如下:

defmodule Xml do
  def start_buffer(state), do: Agent.start_link(fn -> state end)
  def stop_buffer(buffer), do: Agent.stop(buffer)
  def put_buffer(buffer, content), do: Agent.update(buffer, &[content | &1])
  def render(buffer) do
    Agent.get(buffer, &(&1))
    |> Enum.reverse
    |> Enum.join("")
  end
  defmacro xml(do: block) do
    quote do
      {:ok, var!(buffer, Xml)} = start_buffer([])
      put_buffer var!(buffer, Xml), "<?xml version=\"1.0\"?>"
      unquote(block)
      result = render(var!(buffer, Xml))
      :ok = stop_buffer(var!(buffer, Xml))
      result
    end
  end
  defmacro tag(name, do: inner) do
    quote do
      put_buffer var!(buffer, Xml), "<#{unquote(name)}>"
      unquote(inner)
      put_buffer var!(buffer, Xml), "</#{unquote(name)}>"
    end
  end
  defmacro text(string) do
    quote do: put_buffer(var!(buffer, Xml), to_string(unquote(string)))
  end
end

创建一个 Template 模块来使用 Xml 模块:

defmodule Template do
  import Xml
  def render do
    xml do
      tag :model do
        for i <- 0..5 do
          tag :attribute do
            text "some value #{i}"
          end
        end
      end
    end
  end
end

iex 中测试:

iex(1)> c "xml.exs"
[Xml]
iex(2)> c "template.exs"
[Template]
iex(3)> Template.render
"<?xml version=\"1.0\"?><model><attribute>some value 0</attribute><attribute>some value 1</attribute><attribute>some value 2</attribute><attribute>some value 3</attribute><attribute>some value 4</attribute><attribute>some value 5</attribute></model>"

最后,我们添加元素属性的支持。首先定义一个 open_tag 函数:

def open_tag(name, []), do: "<#{name}>"
def open_tag(name, attrs) do
  attr_text = for {k, v} <- attrs, into: "", do: " #{k}=\"#{v}\""
  "<#{name}#{attr_text}>"
end

修改 tag 宏以接受属性:

defmacro tag(name, attrs \\ [], do: inner) do
  quote do
    put_buffer var!(buffer, Xml), open_tag(unquote_splicing([name, attrs]))
    unquote(inner)
    put_buffer var!(buffer, Xml), "</#{unquote(name)}>"
  end
end

更新 Template 模块以使用属性:

xml do
  tag :model, name: "my_model" do
    for i <- 0..5 do
      tag :attribute do
        text "some value #{i}"
      end
    end
  end
end

再次在 iex 中测试,现在可以正确输出带有属性的XML:

iex(1)> c "xml.exs"
[Xml]
iex(2)> c "template.exs"
[Template]
iex(3)> Template.render
"<?xml version=\"1.0\"?><model name=\"my_model\"><attribute>some value 0</attribute><attribute>some value 1</attribute><attribute>some value 2</attribute><attribute>some value 3</attribute><attribute>some value 4</attribute><attribute>some value 5</attribute></model>"

通过以上示例,我们可以看到Elixir宏在不同场景下的强大功能,包括上下文管理、代码生成、测试和DSL创建等。合理使用宏可以提高代码的灵活性和可维护性。

Elixir宏:上下文、卫生性及应用实践(续)

7. 宏应用总结与对比

为了更清晰地理解宏在不同场景下的应用,我们对前面提到的几种应用进行总结和对比,如下表所示:
| 应用场景 | 核心功能 | 关键代码元素 | 优势 |
| — | — | — | — |
| 调试和跟踪 | 自动跟踪库中方法的调用 | Tracer 模块、 def 宏重定义、 dump_args 函数 | 方便调试,可在编译时注入跟踪信息 |
| 静态数据转换为函数 | 从静态数据创建函数 | String.Unicode 模块、列表推导、 Enum.reduce | 支持动态更新,无需修改代码 |
| 测试宏 | 测试生成的代码 | ExUnit.Case 、测试用例编写 | 将测试问题转化为常规模块测试 |
| 特定领域语言(DSL) | 创建小型嵌入式语言 | Ecto 项目、 from...where...select 语法 | 以自然的Elixir术语表达特定问题,编译时检查查询 |
| XML模板DSL | 模板化XML模型 | Xml 模块、 xml / tag / text 宏、 Agent 状态管理 | 简洁地将数据导出为XML,无需额外装饰 |

8. 宏使用的注意事项

虽然宏在Elixir中提供了强大的功能,但使用时也需要注意以下几点:
- 卫生性问题 :如前文所述,宏的上下文和调用者上下文需要仔细处理。不卫生的宏(如使用 var! )虽然能实现一些特殊功能,但会增加代码的复杂性和不可预测性。在使用不卫生宏时,需要充分考虑其对代码的影响。
- 性能影响 :宏在编译时展开,可能会增加编译时间。特别是在处理大量静态数据生成函数或复杂的DSL时,需要注意性能问题。
- 代码可读性 :过度使用宏可能会使代码变得难以理解和维护。在编写宏时,应确保代码的可读性,必要时添加详细的注释。

9. 未来可能的扩展

基于前面的示例和讨论,我们可以设想一些未来可能的扩展方向:
- 更复杂的DSL :可以进一步扩展XML模板DSL,支持更多的XML特性,如命名空间、CDATA等。也可以创建其他领域的DSL,如用于数据分析、机器学习等。
- 宏的组合使用 :将不同类型的宏组合使用,实现更复杂的功能。例如,将调试和跟踪宏与DSL结合,在DSL中添加调试信息。
- 与其他库的集成 :将宏与其他Elixir库集成,如与 Phoenix 框架集成,为Web应用提供更强大的功能。

10. 总结

Elixir宏是一种强大的工具,它允许开发者在编译时注入代码,实现一些在运行时难以实现的功能。通过本文的介绍,我们了解了宏的上下文和卫生性问题,以及如何使用宏进行调试、静态数据处理、测试、创建DSL等。在使用宏时,需要权衡其带来的好处和可能的风险,确保代码的可读性和可维护性。

以下是一个简单的mermaid流程图,展示了创建XML模板DSL的主要流程:

graph TD;
    A[开始] --> B[初始化缓冲区];
    B --> C[处理XML块];
    C --> D{是否有标签};
    D -- 是 --> E[插入标签开始];
    E --> F{是否有内部内容};
    F -- 是 --> G[处理内部内容];
    F -- 否 --> H[插入标签结束];
    G --> H;
    H --> D;
    D -- 否 --> I[渲染结果];
    I --> J[停止缓冲区];
    J --> K[返回结果];
    K --> L[结束];

通过这个流程图,我们可以更直观地看到创建XML模板DSL的主要步骤,包括缓冲区的管理、标签的处理和结果的渲染。希望这些内容能帮助你更好地理解和应用Elixir宏。

内容概要:本文围绕新一代传感器产品在汽车电子电气架构中的关键作用展开分析,重点探讨了智能汽车向高阶智能化演进背景下,传统传感器无法满足感知需求的问题。文章系统阐述了自动驾驶、智能座舱、电动化与网联化三大趋势对传感器技术提出的更高要求,并深入剖析了激光雷达、4D毫米波雷达和3D-ToF摄像头三类核心新型传感器的技术原理、性能优势与现存短板。激光雷达凭借高精度三维点云成为高阶智驾的“眼睛”,4D毫米波雷达通过增加高度维度提升环境感知能力,3D-ToF摄像头则在智能座舱中实现人体姿态识别与交互功能。文章还指出传感器正从单一数据采集向智能决策升级,强调车规级可靠性、多模态融合与成本控制是未来发展方向。; 适合人群:从事汽车电子、智能驾驶、传感器研发等相关领域的工程师和技术管理人员,具备一定专业背景的研发人员;; 使用场景及目标:①理解新一代传感器在智能汽车系统中的定位与技术差异;②掌握激光雷达、4D毫米波雷达、3D-ToF摄像头的核心参数、应用场景及选型依据;③为智能驾驶感知层设计、多传感器融合方案提供理论支持与技术参考; 阅读建议:建议结合实际项目需求对比各类传感器性能指标,关注其在复杂工况下的鲁棒性表现,并重视传感器与整车系统的集成适配问题,同时跟踪芯片化、固态化等技术演进趋势。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值