Elixir 宏扩展问题,但仅限于理解
Elixir macro expansion problems, but only in a comprehension
作为我们 Dev Book Club 工作的一部分,我在 Elixir 中编写了一个随机密码生成器。决定玩元编程,并用宏编写它以稍微干燥一下。
这非常有效:
# lib/macros.ex
defmodule Macros do
defmacro define_alphabet(name, chars) do
len = String.length(chars) - 1
quote do
def unquote(:"choose_#{name}")(chosen, 0) do
chosen
end
def unquote(:"choose_#{name}")(chosen, n) do
alphabet = unquote(chars)
unquote(:"choose_#{name}")([(alphabet |> String.at :random.uniform(unquote(len))) | chosen], n - 1)
end
end
end
end
# lib/generate_password.ex
defmodule GeneratePassword do
require Macros
Macros.define_alphabet :alpha, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
Macros.define_alphabet :special, "~`!@#$%^&*?"
Macros.define_alphabet :digits, "0123456789"
def generate_password(min_length, n_special, n_digits) do
[]
|> choose_alpha(min_length - n_special - n_digits)
|> choose_special(n_special)
|> choose_digits(n_digits)
|> Enum.shuffle
|> Enum.join
end
end
我想在 Dict/map 或什至列表中定义字母表,并对其进行迭代以调用 Macros.define_alphabet,而不是手动调用它 3 次。然而,当我使用下面的代码尝试这个时,无论我使用什么结构来保存字母,它都无法编译。
alphabets = %{
alpha: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
special: "~`!@#$%^&*?",
digits: "0123456789",
}
for {name, chars} <- alphabets, do: Macros.define_alphabet(name, chars)
出现以下错误:
Erlang/OTP 18 [erts-7.1] [source] [64-bit] [smp:8:8] [async-threads:10] [hipe] [kernel-poll:false] [dtrace]
Compiled lib/macros.ex
== Compilation error on file lib/generate_password.ex ==
** (FunctionClauseError) no function clause matching in String.Graphemes.next_grapheme_size/1
(elixir) unicode/unicode.ex:231: String.Graphemes.next_grapheme_size({:chars, [line: 24], nil})
(elixir) unicode/unicode.ex:382: String.Graphemes.length/1
expanding macro: Macros.define_alphabet/2
lib/generate_password.ex:24: GeneratePassword (module)
(elixir) lib/kernel/parallel_compiler.ex:100: anonymous fn/4 in Kernel.ParallelCompiler.spawn_compilers/8
我试过将字母映射为列表列表、元组列表、原子映射->字符串和字符串->字符串,这似乎并不重要。我也试过将这些对传递到 Enum.each 而不是使用 "for" 理解,像这样:
alphabets |> Enum.each fn {name, chars} -> Macros.define_alphabet(name, chars) end
所有这些都给出了相同的结果。认为这可能与调用有关:random.uniform,并将其更改为:
alphabet |> to_char_list |> Enum.shuffle |> Enum.take(1) |> to_string
这只是将错误稍微更改为:
Erlang/OTP 18 [erts-7.1] [source] [64-bit] [smp:8:8] [async-threads:10] [hipe] [kernel-poll:false] [dtrace]
== Compilation error on file lib/generate_password.ex ==
** (Protocol.UndefinedError) protocol String.Chars not implemented for {:name, [line: 24], nil}
(elixir) lib/string/chars.ex:3: String.Chars.impl_for!/1
(elixir) lib/string/chars.ex:17: String.Chars.to_string/1
expanding macro: Macros.define_alphabet/2
lib/generate_password.ex:24: GeneratePassword (module)
(elixir) lib/kernel/parallel_compiler.ex:100: anonymous fn/4 in Kernel.ParallelCompiler.spawn_compilers/8
即使进行了这种更改,当我像在顶部那样手动调用 Macros.define_alphabet 时也能正常工作,但当我以任何形式的理解或使用 Enum.each.
时却无法正常工作
这没什么大不了的,但我希望能够根据用户定义的配置以编程方式在字母表列表中添加和删除。
我敢肯定,随着我对 Metaprogramming Elixir 的深入研究,我将能够解决这个问题,但如果有人有任何建议,我将不胜感激。
列表理解是一种使用一个列表并从中获取另一个列表(或一般情况下为 Enumerable)的方法。在您的情况下,您不想获得新列表,而是想在模块中定义函数。因此,列表推导不是合适的方法。
您可以使用另一个宏来定义地图中的字母表。
想通了。如果我将 bind_quoted 列表传递给引用,无论哪种方式都可以工作,尽管我还没有找到一种方法来预先计算长度并像以前一样使用 :random.uniform ,以避免必须做整个列出每个字符选择的转换。
# lib/macros.ex
defmodule Macros do
defmacro define_alphabet(name, chars) do
quote bind_quoted: [name: name, chars: chars] do
def unquote(:"choose_#{name}")(chosen, 0) do
chosen
end
def unquote(:"choose_#{name}")(chosen, n) do
unquote(:"choose_#{name}")([(unquote(chars) |> to_char_list |> Enum.shuffle |> Enum.take(1) |> to_string) | chosen], n - 1)
end
end
end
end
现在我可以随心所欲地称呼它了:
# lib/generate_password.ex
defmodule GeneratePassword do
require Macros
alphabets = [
alpha: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
special: "~`!@#$%^&*?",
digits: "0123456789",
]
for {name, chars} <- alphabets do
Macros.define_alphabet name, chars
end
# or alphabets |> Enum.map fn {name, chars} -> Macros.define_alphabet name, chars end
# or Macros.define_alphabet :alpha2, "abcd1234"
def generate_password(min_length, n_special, n_digits) do
[]
|> choose_alpha(min_length - n_special - n_digits)
|> choose_special(n_special)
|> choose_digits(n_digits)
|> Enum.shuffle
|> Enum.join
end
end
EDIT 在 4 年多的经验和阅读元编程 Elixir 之后更好的答案。我使用 String.graphemes/1
预拆分字母并使用 Enum.random/1
,我认为后者在 4 年前不存在。
defmodule ChooseFrom do
defmacro __using__(_options) do
quote do
import unquote(__MODULE__)
end
end
defmacro alphabet(name, chars) when is_binary(chars) do
function_name = :"choose_#{name}"
quote do
defp unquote(function_name)(remaining) when is_integer(remaining) and remaining > 0 do
unquote(function_name)([], remaining)
end
defp unquote(function_name)(chosen, remaining) when is_integer(remaining) and remaining > 0 do
next_char = Enum.random(unquote(String.graphemes(chars)))
unquote(function_name)([next_char | chosen], remaining - 1)
end
defp unquote(function_name)(chosen, _), do: chosen
end
end
end
defmodule PasswordGenerator do
use ChooseFrom
alphabet(:alpha, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
alphabet(:digits, "0123456789")
alphabet(:special, "~`!@#$%^&*?")
def generate_password(min_length, num_special, num_digits) do
num_alpha = min_length - num_special - num_digits
num_alpha
|> choose_alpha()
|> choose_special(num_special)
|> choose_digits(num_digits)
|> Enum.shuffle()
|> Enum.join()
end
end
输出:
iex> 1..20 |> Enum.map(fn _ -> PasswordGenerator.generate_password(20, 3, 3) end)
["01?dZQRhrHAbmP*vF3I@", "UUl3O0vqS^S3CQDr^AC$", "%1NOF&Xyh3Cgped*5xnk",
"Scg$oDVUB8Vx&b72GB^R", "SnYN?hlc*D03bW~5Rmsf", "R5Yg6Zr^Jm^!BOCD8Jjm",
"ni^Cg9BBQDne0v`M`2fj", "L8@$TpIUdEN1uy5h@Rel", "6MjrJyiuB26qntl&M%$L",
"hTsDh*y0La?hdhXn7I", "6rq8jeTH%ko^FLMX$g6a", "7jVDS#tjh0GS@q#RodN6",
"dOBi1?4LW%lrr#wG2LIu", "S*Zcuhg~R4!fBoij7y2o", "M!thW*g2Ta&M7o7MpscI",
"r5n3$tId^OWX^KGzjl4v", "L2CLJv&&YwncF6JY*5Zw", "DJWT`f6^3scwCO4pQQ*Q",
"mm2jVh5!J!Zalsuxk8&o", "O#kqGRfHGnu042PS`O*A"]
作为我们 Dev Book Club 工作的一部分,我在 Elixir 中编写了一个随机密码生成器。决定玩元编程,并用宏编写它以稍微干燥一下。
这非常有效:
# lib/macros.ex
defmodule Macros do
defmacro define_alphabet(name, chars) do
len = String.length(chars) - 1
quote do
def unquote(:"choose_#{name}")(chosen, 0) do
chosen
end
def unquote(:"choose_#{name}")(chosen, n) do
alphabet = unquote(chars)
unquote(:"choose_#{name}")([(alphabet |> String.at :random.uniform(unquote(len))) | chosen], n - 1)
end
end
end
end
# lib/generate_password.ex
defmodule GeneratePassword do
require Macros
Macros.define_alphabet :alpha, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
Macros.define_alphabet :special, "~`!@#$%^&*?"
Macros.define_alphabet :digits, "0123456789"
def generate_password(min_length, n_special, n_digits) do
[]
|> choose_alpha(min_length - n_special - n_digits)
|> choose_special(n_special)
|> choose_digits(n_digits)
|> Enum.shuffle
|> Enum.join
end
end
我想在 Dict/map 或什至列表中定义字母表,并对其进行迭代以调用 Macros.define_alphabet,而不是手动调用它 3 次。然而,当我使用下面的代码尝试这个时,无论我使用什么结构来保存字母,它都无法编译。
alphabets = %{
alpha: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
special: "~`!@#$%^&*?",
digits: "0123456789",
}
for {name, chars} <- alphabets, do: Macros.define_alphabet(name, chars)
出现以下错误:
Erlang/OTP 18 [erts-7.1] [source] [64-bit] [smp:8:8] [async-threads:10] [hipe] [kernel-poll:false] [dtrace]
Compiled lib/macros.ex
== Compilation error on file lib/generate_password.ex ==
** (FunctionClauseError) no function clause matching in String.Graphemes.next_grapheme_size/1
(elixir) unicode/unicode.ex:231: String.Graphemes.next_grapheme_size({:chars, [line: 24], nil})
(elixir) unicode/unicode.ex:382: String.Graphemes.length/1
expanding macro: Macros.define_alphabet/2
lib/generate_password.ex:24: GeneratePassword (module)
(elixir) lib/kernel/parallel_compiler.ex:100: anonymous fn/4 in Kernel.ParallelCompiler.spawn_compilers/8
我试过将字母映射为列表列表、元组列表、原子映射->字符串和字符串->字符串,这似乎并不重要。我也试过将这些对传递到 Enum.each 而不是使用 "for" 理解,像这样:
alphabets |> Enum.each fn {name, chars} -> Macros.define_alphabet(name, chars) end
所有这些都给出了相同的结果。认为这可能与调用有关:random.uniform,并将其更改为:
alphabet |> to_char_list |> Enum.shuffle |> Enum.take(1) |> to_string
这只是将错误稍微更改为:
Erlang/OTP 18 [erts-7.1] [source] [64-bit] [smp:8:8] [async-threads:10] [hipe] [kernel-poll:false] [dtrace]
== Compilation error on file lib/generate_password.ex ==
** (Protocol.UndefinedError) protocol String.Chars not implemented for {:name, [line: 24], nil}
(elixir) lib/string/chars.ex:3: String.Chars.impl_for!/1
(elixir) lib/string/chars.ex:17: String.Chars.to_string/1
expanding macro: Macros.define_alphabet/2
lib/generate_password.ex:24: GeneratePassword (module)
(elixir) lib/kernel/parallel_compiler.ex:100: anonymous fn/4 in Kernel.ParallelCompiler.spawn_compilers/8
即使进行了这种更改,当我像在顶部那样手动调用 Macros.define_alphabet 时也能正常工作,但当我以任何形式的理解或使用 Enum.each.
时却无法正常工作这没什么大不了的,但我希望能够根据用户定义的配置以编程方式在字母表列表中添加和删除。
我敢肯定,随着我对 Metaprogramming Elixir 的深入研究,我将能够解决这个问题,但如果有人有任何建议,我将不胜感激。
列表理解是一种使用一个列表并从中获取另一个列表(或一般情况下为 Enumerable)的方法。在您的情况下,您不想获得新列表,而是想在模块中定义函数。因此,列表推导不是合适的方法。
您可以使用另一个宏来定义地图中的字母表。
想通了。如果我将 bind_quoted 列表传递给引用,无论哪种方式都可以工作,尽管我还没有找到一种方法来预先计算长度并像以前一样使用 :random.uniform ,以避免必须做整个列出每个字符选择的转换。
# lib/macros.ex
defmodule Macros do
defmacro define_alphabet(name, chars) do
quote bind_quoted: [name: name, chars: chars] do
def unquote(:"choose_#{name}")(chosen, 0) do
chosen
end
def unquote(:"choose_#{name}")(chosen, n) do
unquote(:"choose_#{name}")([(unquote(chars) |> to_char_list |> Enum.shuffle |> Enum.take(1) |> to_string) | chosen], n - 1)
end
end
end
end
现在我可以随心所欲地称呼它了:
# lib/generate_password.ex
defmodule GeneratePassword do
require Macros
alphabets = [
alpha: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
special: "~`!@#$%^&*?",
digits: "0123456789",
]
for {name, chars} <- alphabets do
Macros.define_alphabet name, chars
end
# or alphabets |> Enum.map fn {name, chars} -> Macros.define_alphabet name, chars end
# or Macros.define_alphabet :alpha2, "abcd1234"
def generate_password(min_length, n_special, n_digits) do
[]
|> choose_alpha(min_length - n_special - n_digits)
|> choose_special(n_special)
|> choose_digits(n_digits)
|> Enum.shuffle
|> Enum.join
end
end
EDIT 在 4 年多的经验和阅读元编程 Elixir 之后更好的答案。我使用 String.graphemes/1
预拆分字母并使用 Enum.random/1
,我认为后者在 4 年前不存在。
defmodule ChooseFrom do
defmacro __using__(_options) do
quote do
import unquote(__MODULE__)
end
end
defmacro alphabet(name, chars) when is_binary(chars) do
function_name = :"choose_#{name}"
quote do
defp unquote(function_name)(remaining) when is_integer(remaining) and remaining > 0 do
unquote(function_name)([], remaining)
end
defp unquote(function_name)(chosen, remaining) when is_integer(remaining) and remaining > 0 do
next_char = Enum.random(unquote(String.graphemes(chars)))
unquote(function_name)([next_char | chosen], remaining - 1)
end
defp unquote(function_name)(chosen, _), do: chosen
end
end
end
defmodule PasswordGenerator do
use ChooseFrom
alphabet(:alpha, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
alphabet(:digits, "0123456789")
alphabet(:special, "~`!@#$%^&*?")
def generate_password(min_length, num_special, num_digits) do
num_alpha = min_length - num_special - num_digits
num_alpha
|> choose_alpha()
|> choose_special(num_special)
|> choose_digits(num_digits)
|> Enum.shuffle()
|> Enum.join()
end
end
输出:
iex> 1..20 |> Enum.map(fn _ -> PasswordGenerator.generate_password(20, 3, 3) end)
["01?dZQRhrHAbmP*vF3I@", "UUl3O0vqS^S3CQDr^AC$", "%1NOF&Xyh3Cgped*5xnk",
"Scg$oDVUB8Vx&b72GB^R", "SnYN?hlc*D03bW~5Rmsf", "R5Yg6Zr^Jm^!BOCD8Jjm",
"ni^Cg9BBQDne0v`M`2fj", "L8@$TpIUdEN1uy5h@Rel", "6MjrJyiuB26qntl&M%$L",
"hTsDh*y0La?hdhXn7I", "6rq8jeTH%ko^FLMX$g6a", "7jVDS#tjh0GS@q#RodN6",
"dOBi1?4LW%lrr#wG2LIu", "S*Zcuhg~R4!fBoij7y2o", "M!thW*g2Ta&M7o7MpscI",
"r5n3$tId^OWX^KGzjl4v", "L2CLJv&&YwncF6JY*5Zw", "DJWT`f6^3scwCO4pQQ*Q",
"mm2jVh5!J!Zalsuxk8&o", "O#kqGRfHGnu042PS`O*A"]