Elixir宏扩展问题,但只是在理解

Chr*_*ett 6 macros elixir

作为我们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
Run Code Online (Sandbox Code Playgroud)

我想在Dict/map或甚至列表中定义字母表,并迭代它以调用Macros.define_alphabet,而不是手动调用它3次.但是,当我尝试这个时,使用下面的代码,它无法编译,无论我用什么结构来保存字母表.

alphabets = %{
  alpha: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
  special:  "~`!@#$%^&*?",
  digits: "0123456789",
}

for {name, chars} <- alphabets, do: Macros.define_alphabet(name, chars)
Run Code Online (Sandbox Code Playgroud)

给出以下错误:

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
Run Code Online (Sandbox Code Playgroud)

我已经尝试将字母表映射为列表列表,元组列表,原子映射 - >字符串和字符串 - >字符串,它似乎并不重要.我也尝试将这些对用于Enum.each而不是使用"for"理解,如下所示:

alphabets |> Enum.each fn {name, chars} -> Macros.define_alphabet(name, chars) end
Run Code Online (Sandbox Code Playgroud)

所有这些都给出了相同的结果.认为它可能与调用:random.uniform有关,并将其更改为:

alphabet |> to_char_list |> Enum.shuffle |> Enum.take(1) |> to_string
Run Code Online (Sandbox Code Playgroud)

这只是稍微改变了错误,:

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
Run Code Online (Sandbox Code Playgroud)

即使有了这个改变,当我在顶部手动调用Macros.define_alphabet时工作正常,但是当我在任何类型的理解或使用Enum.each时都没有.

这不是什么大不了的事,但我希望能够以编程方式添加到字母表列表中并从中删除,具体取决于用户定义的配置.

我确信随着我进一步进入Metaprogramming Elixir,我将能够解决这个问题,但如果有人有任何建议,我会很感激.

Chr*_*ett 2

弄清楚了。如果我传递 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
Run Code Online (Sandbox Code Playgroud)

现在我可以用我喜欢的任何方式来称呼它:

# 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
Run Code Online (Sandbox Code Playgroud)

编辑经过 4 年以上的经验和阅读元编程 Elixir 后得到更好的答案。String.graphemes/1我使用和 use预先分割了字母表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
Run Code Online (Sandbox Code Playgroud)

输出:

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",
 "$9hTsDh*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"]
Run Code Online (Sandbox Code Playgroud)