Elixir macro expansion problems, but only in understanding

As part of our Dev Book Club at work, I wrote a random password generator at Elixir. I decided to play with metaprogramming and write it using macros in DRY a bit.

This works great:

# 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 

I would like to define alphabets in a Dict / map or even a list and iterate over them to call Macros.define_alphabet, instead of calling it 3 times manually. However, when I try to do this using the code below, it does not compile, no matter what structure I use to store the alphabets.

 alphabets = %{ alpha: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", special: "~` !@ #$%^&*?", digits: "0123456789", } for {name, chars} <- alphabets, do: Macros.define_alphabet(name, chars) 

Providing the following error:

 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 

I tried having a map of alphabets like a list of lists, a list of tuples, a map of atoms β†’ strings and strings β†’ strings, and that doesn't seem to matter. I also tried connecting pairs to Enum.each instead of using a β€œfor” understanding, for example:

 alphabets |> Enum.each fn {name, chars} -> Macros.define_alphabet(name, chars) end 

All of them give the same results. I thought that this could be due to the call: random.uniform and changed like this:

 alphabet |> to_char_list |> Enum.shuffle |> Enum.take(1) |> to_string 

This will slightly modify the error so that:

 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 

Even with this change, it works fine when I manually call Macros.define_alphabet, as above, but not when I do it in any way or using Enum.each.

This is not a huge deal, but I want to be able to programmatically add and remove from the list of alphabets depending on the user configuration.

I am sure that as I move forward with Metaprogramming Elixir , I can understand this, but if anyone has any suggestions, I would appreciate it.

+6
source share
2 answers

To guess. It works anyway if I pass the bind_quoted list in quotation marks, although I have not found a way to pre-calculate the length and use: random.uniform, as I did before, to avoid having to convert the entire list for each character selection.

 # 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 

And now I can call it what I like:

 # 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 The best answer after 4 more years of experience and reading the metaprogramming elixir. I previously split the alphabets using String.graphemes/1 , and using Enum.random/1 , the last of which, I don't think, existed 4 years ago.

 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 

Output:

 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"] 
+2
source

List comprehensions is a way of using one list and getting another list (or Enumerable in general) from it. In your case, you do not want to receive a new list, you want to define functions in the module. Therefore, understanding lists is not suitable for this.

You can use another macro to define alphabets from the map.

+2
source

All Articles