elixir 之宏

关键字

require

1
导入宏, 否则不能调用宏的方法

use

给模块动态增加方法

相当于

1
2
require XXX
XXX.__using__

quote

生成当前表达式的语法树,必须要在 defmacro 语句的里面才能用

unquote

说明

1
把 语法树转换为实际表达式,一般和 `quote` 一起组合成为新的 ast,必须在宏里面才能用

使用场景

1
defmacro 关键字修饰的函数参数,如果要使用它作为变量,则需要 `unquote`

宏调试

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
defmodule SimpleMacro do
defmacro plus(ast_x, ast_y) do
quote do
unquote(ast_x) + unquote(ast_y)
end
end
end

ast =
quote do
SimpleMacro.plus(x, 23)
end
ast |> Macro.expand(__ENV__) |> Macro.to_string

基础用法

普通例子

1
2
3
4
5
6
7
defmodule DemoMacro do
defmacro plus(ast_x, ast_y) do
quote do
unquote(ast_x) + unquote(ast_y)
end
end
end

测试

1
2
require DemoMacro
DemoMacro.plus(11,22)

bind_quoted

bind_quoted 绑定的值会保持不变

代码如下

1
2
3
4
5
6
7
8
9
10
defmodule DemoMacro do
require Logger

defmacro double_puts(expr) do
quote do
Logger.debug(unquote(expr))
Logger.debug(unquote(expr))
end
end
end
1
2
3
4
5
6
7
8
9
defmodule DemoMacro do
require Logger

defmacro double_puts(expr) do
quote bind_quoted: [aaa: expr] do
Logger.debug(aaa)
end
end
end

测试

1
2
require DemoMacro
DemoMacro.double_puts(:os.system_time)

使用场景

参考链接

动态添加方法和属性

use 模块除了在模块调用 __using__ 宏外, 不做其他任何事情

注意 require 的位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
defmodule DemoMacro do
defmacro demo(name) do
quote do
unquote(name) <> ", " <> "in DemoMacro"
end
end
end

defmodule DemoUsing do
defmacro __using__(opts) do
params = Keyword.get(opts, :aaa, "default params")

quote do
require DemoMacro
def demo(name) do
value = "value from " <> unquote(params) <> ", " <> name
DemoMacro.demo(value)
end
end
end
end

defmodule Demo do
use DemoUsing, aaa: "123"
end

测试

1
Demo.demo("me")

方法重写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
defmodule DemoUsing do
defmacro __using__(_opts) do

quote do
def test1(x, y) do
x + y
end

def test2(x, y) do
x - y
end
defoverridable test1: 2, test2: 2
end
end
end

defmodule Demo do
use DemoUsing

def test1(x, y) do
# 调用默认实现
super(x, y)
end

def test2(x, y) do
x * y * y
end
end

测试

1
2
Demo.test1(11, 22)
Demo.test2(11, 22)

behaviour 重写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
defmodule DemoBehaviour do
@callback demo(number(), number()) :: number()
end

# defoverridable 使用 Behaviour 当参数的时候,所有 callback 都可以被 override
defmodule DemoUsing do
require Logger

defmacro __using__(_opts) do

quote do
@behaviour DemoBehaviour
def demo(x, y) do
Logger.debug("default in DemoBehaviour")
x + y
end

defoverridable DemoBehaviour
end
end
end

defmodule Demo do
require Logger
use DemoUsing

def demo(x, y) do
# super(x, y)
Logger.debug("override in DemoBehaviour")
x * y
end
end

测试

1
Demo.demo(11, 22)

动态生成常量

模块编译完成以后,会打印 [“henry”, “john”]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
defmodule DemoMacro do
defmacro add(name) do
quote do
@names unquote(name)
:ok
end
end
end

defmodule Demo do
require DemoMacro
import DemoMacro

Module.register_attribute(__MODULE__, :names, accumulate: true)

add "john"
add "henry"

IO.inspect(@names)
end

修改属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
defmodule DemoUsing do
defmacro __using__(_) do
quote do
Module.register_attribute(__MODULE__, :xxx, accumulate: true)
end
end

defmacro show(name) do
quote do
IO.inspect("#{unquote(name)} #{@xxx}")
end
end
end

defmodule Demo do
use DemoUsing

@xxx "测试"

def aaa() do
DemoUsing.show("123")
end
end

编译期 hook

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
defmodule DemoUsing do
defmacro __using__(_) do
quote do
@before_compile unquote(__MODULE__)
end
end

defmacro __before_compile__(env) do
IO.inspect(env)
nil
end
end

defmodule Demo do
use DemoUsing
end

模拟 test

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
defmodule Calculator do
def add(a, b), do: a + b
def subtract(a, b), do: a - b
end

defmodule TestCase do
defmacro __using__(_) do
quote do
require Logger
import TestCase
Module.register_attribute(__MODULE__, :tests, accumulate: true)
@before_compile unquote(__MODULE__)
end
end

defmacro __before_compile__(_env) do
# Inject a run function into the test case after all tests have been accumulated
quote do
def run do
Enum.each @tests, fn test_name ->
result = apply(__MODULE__, test_name, [])
state = if result, do: "pass", else: "fail"
Logger.debug("#{test_name} => #{state}")
end
end
end
end

defmacro test(description, do: body) do
test_name = String.to_atom(description)
quote do
@tests unquote(test_name)
def unquote(test_name)(), do: unquote(body)
end
end
end

defmodule CalculatorTest do
use TestCase
import Calculator

test "add 1, 2 should return 3" do
add(1, 2) == 3
end

test "subtract 5, 2 should not return 4" do
subtract(5, 2) == 4
end
end

CalculatorTest.run

erlang 之添加依赖

说明

不能加载 elixir 的库

用法

远程依赖

hex.pm

1
2
3
{deps,[
{jsx, "3.1.0"}
]}.

分支

1
2
3
{deps, [
{ets_cache, ".*", {git, "https://github.com/roowe/ets_cache", {branch, "master"}}}
]}.

tag

1
2
3
{deps, [
{erlbus, {git, "https://github.com/cabol/erlbus", {tag, "v0.3.0"}}}
]}.

本地依赖

目录层级

1
2
3
4
5
6
7
.
├── demo
│   ├── rebar.config
│   └── src
└── demo_lib
├── rebar.config
└── src
1
2
3
4
5
6
7
{deps, [
{demo_lib, {path, "../demo_lib"}}
]}.

{plugins, [
rebar3_path_deps
]}.

elixir 之添加依赖

说明

可以加载 erlang 的库

搜索

1
mix hex.search --package nebulex

用法

加载本地库

1
{:xxx, path: "../xxx"}

指定分支

1
{:bumblebee, github: "elixir-nx/bumblebee", branch: "main"}

强制使用某个版本

1
{:bumblebee, github: "elixir-nx/bumblebee", branch: "main", override: true}

指定环境下加载

1
{:exla, "~> 0.7", only: [:dev, :test]}

运行时并不加载

1
{:ex_doc, "~> 0.29", only: :dev, runtime: false}

elixir 项目类型

app

带 Application

例子

1
mix new demo --sup

验证

1
Application.started_applications

escript

可执行脚本,需要本机安装 elixir

例子

1
mix new demo

mix.exs

1
2
3
4
5
6
7
8
9
10
11
12
13
def project do
[
...
escript: escript()
]
end

defp escript do
[
main_module: DemoApp,
path: "_build/escripts/aaa",
]
end
1
2
3
4
5
6
defmodule DemoApp do
require Logger
def main(args) do
Logger.debug("args #{inspect(args)}")
end
end

发布

1
mix escript.build

lib

默认的项目类型就是 lib

例子

1
mix new demo

umbrella

项目大了以后,db 和 web 等希望独立出来

用法

创建

1
2
3
4
5
6
7
8
9
10
mix new umbrella_demo --umbrella
cd umbrella_demo/apps

# 普通子项目
mix new demo1

# phoenix 子项目
mix phx.new.web demo_web
# ecto 子项目
mix phx.new.ecto demo_ecto

依赖

共享依赖,放在顶层的 mix.exs

子项目依赖,放在子项目里面的 mix.exs

配置

默认只读取最外层配置,建议拆分

在顶层 config 目录里面拆,具体见配置分离文档

app 自启动

需要自动启动的 application, 在子项目的 mix.exs 里面修改

elixir 之配置文件拆分

说明

配置太多,需要拆分

目录结构

1
2
3
4
5
6
7
8
9
10
├── config.exs
├── dev
│ ├── api.exs
│ └── bot.exs
├── prod
│ └── api.exs
├── runtime
│ └── api.exs
└── test
└── api.exs

config.exs

1
2
3
4
5
6
7
8
# import_config "#{config_env()}.exs"

# umbrella 项目的话,在顶层 config 目录内拆分
for config <- "#{config_env()}/*.exs" |> Path.expand(__DIR__) |> Path.wildcard() do
if File.exists?(config) do
import_config config
end
end

elixir 之配置文件

说明

  1. runtime 为运行时会调用的配置文件, 其它为编译期调用的配置
  2. 固定的配置写在 dev/prod 等里面
  3. 动态配置,写在 runtime 里面,如果 runtime 里面没有,则从 dev/prod 里面去取

例子

目录结构

1
2
3
4
5
6
7
8
lib
└── demo.exs
config
├── config.exs
├── dev.exs
├── test.exs
├── prod.exs
└── runtime.exs

配置

config.exs

1
2
3
4
import Config

config :demo, mode: config_env()
import_config "#{config_env()}.exs"

dev.exs

1
2
3
4
5
6
7
8
9
import Config

config :demo,
aaa: "aaa in dev",
bbb: "bbb in dev"

config :demo, MyModule,
ccc: "ccc in dev",
ddd: "ddd in dev"

test.exs

1
2
3
4
5
6
7
8
9
import Config

config :demo,
aaa: "aaa in test",
bbb: "bbb in test"

config :demo, MyModule,
ccc: "ccc in test",
ddd: "ddd in test"

prod.exs

1
2
3
4
5
6
7
8
9
import Config

config :demo,
aaa: "aaa in prod",
bbb: "bbb in prod"

config :demo, MyModule,
ccc: "ccc in prod",
ddd: "ddd in prod"

runtime.exs

1
2
3
4
5
6
7
import Config

# 这个只能用于配置文件,代码内不能使用这个方式动态处理代码
if config_env() == :prod do
config :demo,
aaa: System.get_env("AAA") || "aaa in runtime"
end

测试

1
MIX_ENV=prod iex -S mix
1
2
3
4
Application.fetch_env!(:demo, :aaa)
Application.get_env(:demo, :bbb)

[ccc: ccc_value, ddd: ddd_value] = Application.get_env(:demo, MyModule)

备注

如果需要在运行的时候获取当前模式

1
2
3
if Application.get_env(:demo, :mode) === :prod do
xxxx
end

elixir 之 cond 判断

说明

都不支持多 else

匹配条件可以用函数

用法

cond

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
info1 = ""
info2 = ""

cond do
String.length(info1) !== 0 ->
"info1 not 0"

String.length(info2) !== 0 ->
"info2 not 0"

String.length(info1) === 0 && String.length(info2) === 0 ->
"len are 0"

true ->
"default"
end

if

1
2
3
4
5
if String.valid?("aaa") && String.valid?("bbb") do
"Valid string!"
else
"Invalid string."
end

elixir 之 match 判断

说明

都不支持多 else

用法

case

  • 待匹配变量必须先定义
  • 匹配条件不可以用函数
1
2
3
4
5
6
7
8
9
10
11
require Logger

case xxx("aaa") do
"aaa" ->
Logger.debug("aaa")

"bbb" ->
Logger.debug("bbb")
_ ->
Logger.debug("default")
end

with

  • 链式匹配,所以待匹配变量无需定义
  • 如果某个 step 失败,之前的还是会成功执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
defmodule WithDemo do
require Logger

def demo do
with {:ok, result1} <- step1(),
{:ok, result2} <- step2(result1),
{:ok, result3} <- step3(result2) do
Logger.debug("final result #{result1 + result2 + result3}")
else
{:error, _} ->
Logger.debug("某些需要匹配的错误")

_ ->
Logger.debug("都不匹配,到这里")
end
end

def step1() do
Logger.debug("in step1")
{:ok, 111}
end

def step2(arg) do
Logger.debug("in step2 #{inspect(arg)}")
{:ok, 222}
end

def step3(arg) do
Logger.debug("in step2 #{inspect(arg)}")
{:ok, 333}
end
end

elixir 之节点可视化

Phoenix LiveDashboard,从 phoenix 独立出来了

步骤

安装

1
mix escript.install hex plds

使用

1
2
iex --sname aaa@localhost --cookie 123456
plds server --connect aaa@localhost --cookie 123456 --port 9000 --open

注意右上角的节点,别选错了

elixir phoenix 应用原生发布

参考地址

例子

1
2
3
4
5
6
7
8
mix deps.get --only prod
MIX_ENV=prod mix compile

MIX_ENV=prod mix assets.deploy
# MIX_ENV=prod mix ecto.migrate
MIX_ENV=prod mix release
export SECRET_KEY_BASE=`mix phx.gen.secret` PHX_SERVER=true PHX_HOST="aaa.com" PORT=9876;
_build/prod/rel/web_demo/bin/web_demo start