文档章节

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

ljzn
 ljzn
发布于 2016/08/14 10:37
字数 1555
阅读 45
收藏 0

#领域特定语言

  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的文档中找到关于宏和编译环境的有用信息.

© 著作权归作者所有

共有 人打赏支持
ljzn
粉丝 29
博文 69
码字总数 96245
作品 0
南平
程序员
私信 提问
elixir官方入门教程 学习资料

下一步该去哪 构建你的第一个Elixir项目 元编程 社区与其它资源 Erlang基础 想要学习更多?继续阅读! 构建你的第一个Elixir项目 为了开始你的第一个项目,Elixir装载了一个叫做Mix的构建工具....

ljzn
2016/08/06
144
0
elixir官方教程 元编程(一) 引用与去引用

引用与去引用 引用(Quoting) 去引用(Unquoting) 释放(Escaping) 一个Elixir程序可以用它自己的数据结构来表现.本章,我们将会学习这些结构体的特点和如何组成它们.本章我们要学习的概念是为宏...

ljzn
2016/08/13
44
0
Elixir学习笔记(模型匹配、控制语句)

模型匹配 模式匹配是 Elixir 很强大的特性,它允许我们匹配简单值、数据结构、甚至函数。 匹配运算符 在Elixir中,运算符实际上叫做 匹配运算符。通过这个匹配操作符,我们可以赋值和匹配值。...

程序员小哥哥
04/12
0
0
elixir官方教程 元编程(二) 宏

宏 前言 我们的第一个宏 宏的隔离 环境 私有宏 负责任地编写宏 前言 尽管Elixir已竭力为宏提供一个安全的环境,用宏编写干净代码的责任仍然落在了开发者身上.宏比传统的Elixir函数更难编写,而...

ljzn
2016/08/14
1K
3
elixir官方入门教程 别名,要求与进口

别名,要求与进口 别名 要求 进口 使用 理解别名 模块嵌套 群体别名/进口/要求/使用 为了方便软件复用,Elixir提供了三个命令(,和)外加一个宏,简介如下: 现在我们将详细探索它们.记住前三条之所...

ljzn
2016/08/04
255
2

没有更多内容

加载失败,请刷新页面

加载更多

python机器学习及实践学习笔记1-如何打开ipynb后缀文件

python机器学习及实践学习笔记1-如何打开ipynb后缀文件 2017年02月22日 14:58:08 hustzhoutian 阅读数:45365更多 个人分类: 深度学习 需要安装ipython notebook,如果你已经安装Anaconda软...

linjin200
4分钟前
0
0
关于在vim中的查找和替换

1,查找 在normal模式下按下/即可进入查找模式,输入要查找的字符串并按下回车。 Vim会跳转到第一个匹配。按下n查找下一个,按下N查找上一个。 Vim查找支持正则表达式,例如/vim$匹配行尾的"...

休辞醉倒
9分钟前
0
0
in_array的坑

PHP in_array的坑 ps: 应该是弱类型语言的坑 php文档 顾名思义,in_array就是查找一个值是否在数组里面。 问题 事故现场 一个sql注入的测试代码如下: $type = $_GET['type'];$types = [2,3,...

o0无忧亦无怖
9分钟前
11
1
Yarn(包管理器) 的基本用法

Yarn是一个快速、可靠、安全的依赖管理工具,是npm的代替品。 Yarn对你的代码来说是一个包管理工具,你可以通过它使用全世界开发者的代码,或者分享自己的代码。 安装Yarn: 操作系统不同,安...

帝子兮
10分钟前
0
0
阿里云HBase全新发布X-Pack NoSQL数据库再上新台阶

一、八年双十一,造就国内最大最专业HBase技术团队 阿里巴巴集团早在2010开始研究并把HBase投入生产环境使用,从最初的淘宝历史交易记录,到蚂蚁安全风控数据存储。持续8年的投入,历经8年双...

阿里云官方博客
10分钟前
0
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部