【问题标题】:Elixir macro expansion problems, but only in a comprehensionElixir 宏扩展问题,但仅在一个理解中
【发布时间】:2016-03-21 21:20:34
【问题描述】:

作为我们 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

我想在字典/地图甚至列表中定义字母,并对其进行迭代以调用 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,我将能够解决这个问题,但如果有人有任何建议,我将不胜感激。

【问题讨论】:

  • 我不知道这个问题的答案,但这可能会对您有所帮助:当您将参数传递给宏时,您传递的是 AST 表示。如果您传递 "string":atom,它们将作为文字传递,而当您传递变量时,它们将作为变量在 Elixir AST 中的表示方式传递:{:name, [line: 24], Elixir}
  • @davoclavo:看起来这就是问题所在。制作了第二个使用列表/字典的宏,它工作得很好,除非(再次)列表是一个变量。将它作为变量传递给“(Protocol.UndefinedError)协议枚举未实现{:alphabets,[line:14],nil}”,所以看起来它得到了Macro.var的结果,我不是确定如何扩展为实际值,或者我什至应该这样做。

标签: macros elixir


【解决方案1】:

列表推导是一种使用一个列表并从中获取另一个列表(或一般情况下为 Enumerable)的方法。在您的情况下,您不想获得新列表,而是想在模块中定义函数。因此,列表推导式不是合适的方法。

您可以使用另一个宏来定义地图中的字母。

【讨论】:

    【解决方案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
    

    现在我可以随心所欲地称呼它了:

    # 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
    

    编辑在 4 年以上的经验和阅读 Metaprogramming 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",
     "$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"]
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2020-05-29
      • 1970-01-01
      • 1970-01-01
      • 2022-01-13
      • 1970-01-01
      • 1970-01-01
      • 2014-11-26
      相关资源
      最近更新 更多