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
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.