elixir官方教程 元编程(三) 领域特定语言

#领域特定语言

  1. 前言
  2. 构建我们的测试案例
  3. test
  4. 用属性存储信息

#前言

领域特定语言(DSL)允许开发者修改他们的应用以适应特定的领域.制作DSL可以不需要用到宏:你在你的模块中定义的每个数据结构和每个函数都是你的领域特定语言的一部分.

比如,假设你想要实现一个提供了数据验证领域特定语言的验证模块.你可以使用数据结构,函数或宏来实现它.让我们来看看这些不同的DSL:

# 1. data structures
import Validator
validate user, name: [length: 1..100],
               email: [matches: ~r/@/]

# 2. functions
import Validator
user
|> validate_length(:name, 1..100)
|> validate_matches(:email, ~r/@/)

# 3. macros + modules
defmodule MyValidator do
  use Validator
  validate_length :name, 1..100
  validate_matches :email, ~r/@/
end

MyValidator.validate(user)

上述方法中,第一个是最有弹性的.如果我们的领域规则可以使用数据结构来编码,它们组成和实现起来会非常简单,因为Elixir的标准库中有很多用于操作不同数据类型的函数.

第二个方法使用函数调用,它能更好地适应更复杂的API(例如,需要传递很多选项)而且归功于Elixir中的管道操作符,它更易读.

第三个方法,使用宏,无疑是最复杂的.需要更多行的代码来实现,测试它是很难且很昂贵的(与测试简单的函数相比),而且它也限制了用户使用库的方法,因为所有验证需要在一个模块中定义.

为了更好地理解,假设你想要只在某种情况下验证某个特定属性.我们可以简单地使用第一个方法操作相应的数据结构来实现,或者使用第二个方法在调用函数前使用条件语句(if/else).然而不可能使用宏来完成,除非该DSL已经增强过了.

也就是说:

data > functions > macros

仍然有一些时候,由宏和模块来构建领域特定语言是很管用的.因为我们已经在入门教程中探索过了数据结构和函数定义,本章将会探索如何使用宏和模块属性来处理更复杂的DSL.

#构建我们自己的测试案例

本章的目标是构建一个名为TestCase的模块,它允许我们这样编写:

defmodule MyTest do
  use TestCase

  test "arithmetic operations" do
    4 = 2 + 2
  end

  test "list operations" do
    [1, 2, 3] = [1, 2] ++ [3]
  end
end

MyTest.run

在上面的例子中,使用TestCase,我们可以用test宏来编写测试,它定义了一个名为run的函数来自动为我们运行所有测试.我们的原型将简单地依赖匹配操作符(=)作为一个断言机制.

#test

让我们创建一个模块,它在被使用时简单地定义并进口了test宏:

defmodule TestCase do
  # Callback invoked by `use`.
  #
  # For now it simply returns a quoted expression that
  # imports the module itself into the user code.
  @doc false
  defmacro __using__(_opts) do
    quote do
      import TestCase
    end
  end

  @doc """
  Defines a test case with the given description.

  ## Examples

      test "arithmetic operations" do
        4 = 2 + 2
      end

  """
  defmacro test(description, do: block) do
    function_name = String.to_atom("test " <> description)
    quote do
      def unquote(function_name)(), do: unquote(block)
    end
  end
end

假设我们在一个名为tests.exs的文件中定义了TestCase,我们可以通过运行iex tests.exs打开它,并定义我们的第一个测试:

iex> defmodule MyTest do
...>   use TestCase
...>
...>   test "hello" do
...>     "hello" = "world"
...>   end
...> end

现在我们还没有一个运行测试的机制,但是我们知道有一个名为test hello的函数已经在幕后被定义了.当调用它时,他应该会失败:

iex> MyTest."test hello"()
** (MatchError) no match of right hand side value: "world"

#使用属性存储信息

为了完整实现我们的TestCase,我们需要能够访问所有已经定义的测试案例.我们可以通过在运行时使用__MODULE__.__info__(:functions)来检索测试,它会返回一个包含给定模块中所有函数的列表.然而,考虑到我们可能需要存储更多关于每个测试的信息,这就要求要有一个更灵活地方法.

当在前几章中讨论模块属性时,我们提到了它们是如何被用作临时存储的.那就是我们在本节中将应用的特性.

__using__/1的实现中,我们将会把一个名为@tests的模块属性初始化成一个空列表,然后存储每个已定义测试的名字到该属性中,这样测试就可以从run函数调用.

这是TestCase模块更新后的代码:

defmodule TestCase do
  @doc false
  defmacro __using__(_opts) do
    quote do
      import TestCase

      # Initialize @tests to an empty list
      @tests []

      # Invoke TestCase.__before_compile__/1 before the module is compiled
      @before_compile TestCase
    end
  end

  @doc """
  Defines a test case with the given description.

  ## Examples

      test "arithmetic operations" do
        4 = 2 + 2
      end

  """
  defmacro test(description, do: block) do
    function_name = String.to_atom("test " <> description)
    quote do
      # Prepend the newly defined test to the list of tests
      @tests [unquote(function_name) | @tests]
      def unquote(function_name)(), do: unquote(block)
    end
  end

  # This will be invoked right before the target module is compiled
  # giving us the perfect opportunity to inject the `run/0` function
  @doc false
  defmacro __before_compile__(env) do
    quote do
      def run do
        Enum.each @tests, fn name ->
          IO.puts "Running #{name}"
          apply(__MODULE__, name, [])
        end
      end
    end
  end
end

通过启动一个新的IEx会话,我们现在可以定义并运行我们的测试:

iex> defmodule MyTest do
...>   use TestCase
...>
...>   test "hello" do
...>     "hello" = "world"
...>   end
...> end
iex> MyTest.run
Running test hello
** (MatchError) no match of right hand side value: "world"

尽管我们跳过了一些细节,但这就是在Elixir中创建领域特定模块的主要思想.宏使得我们能够返回在调用者中执行了的引用表达式,我们可以用它来变形代码并通过模块属性来在目标模块中存储相关信息.最后,@before_compile这样的回调能让我们将代码注入到已定义完成的模块中.

除了@before_compile,还有其它有用的模块属性,例如@on_definition@after_compile,你可以在Module模块的文档中获取有关它们的信息.你也可以在Macro模块和Macro.Env的文档中找到关于宏和编译环境的有用信息.

转载于:https://my.oschina.net/ljzn/blog/732360

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Table of Contents 第⼀部分:基础 Introduction 基础集合Enum 模块 模式匹配 控制语句 函数管道操作符 模块(Module) Mix 魔符(Sigil) ⽂档模块 测试推导字符串 ⽇期和时间 ⾃定义Mix任务 IEx辅助函数 第⼆部分:⾼级 1 2.1 2.2 2.3 2.4 2.5 2.6 2.7 2.8 2.9 2.10 2.11 2.12 2.13 2.14 3.1 3.2 3.3 3.4 4.1 4.2 4.3 和Erlang互操作 错误处理 可执⾏⽂件 并发OTP并发 OTP Supervisors OTP 分布式 编程 Umbrella Projects Specifications and types ⾏为GenStage 协议Nerves 第部分:ECTO Basics Changesets 关联关系 查询第四部分:专题 Plug 嵌⼊的 Elixir(EEx) Erlang 项式存储(ETS) 2 4.4 4.5 5.1 5.2 5.3 5.4 5.5 5.6 Mnesia 数据库 调试第五部分:程序库 Guardian(基础) Poolboy Benchee Bypass Distillery(基础) StreamData 3 Introduction 绪⾔第⼀部分:基础 基础集合Enum 模块 模式匹配 控制语句 函数管道操作符 模块(Module) Mix 魔符(Sigil) ⽂档模块 测试推导字符串 ⽇期和时间 ⾃定义Mix任务 IEx辅助函数 第⼆部分:⾼级 和Erlang互操作 错误处理 可执⾏⽂件 并发OTP并发 OTP Supervisors 4 Introduction OTP 分布式 编程 Umbrella Projects Specifications and types ⾏为GenStage 协议Nerves 第部分:ECTO Basics Changesets 关联关系 查询 第四部分:专题 Plug 嵌⼊的 Elixir(EEx) Erlang 项式存储(ETS) Mnesia 数据库 调试 第五部分:程序库 Guardian(基础) Poolboy Benchee Bypass Distillery(基础) StreamData

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值