As part of our Dev Book Club at work, I wrote a random password generator in Elixir. Decided to play with metaprogramming, and write it with macros to DRY things up a bit.
This works perfectly:
# 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'd like to define the alphabets in a Dict/map, or even a list, and iterate over that to call Macros.define_alphabet, rather than calling it 3 times manually. However, when I try this, using the code below, it fails compilation, no matter what structure I use to hold the alphabets.
alphabets = %{
alpha: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
special: "~`!@#$%^&*?",
digits: "0123456789",
}
for {name, chars} <- alphabets, do: Macros.define_alphabet(name, chars)
Giving 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've tried having the alphabets map as a list of lists, list of tuples, a map of atoms->strings and strings->strings, and it doesn't seem to matter. I've also tried piping the pairs into Enum.each instead of using the "for" comprehension, like so:
alphabets |> Enum.each fn {name, chars} -> Macros.define_alphabet(name, chars) end
All of them give the same results. Thought it might be something to do with calling :random.uniform, and changed that to:
alphabet |> to_char_list |> Enum.shuffle |> Enum.take(1) |> to_string
That just changes the error slightly, to:
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 that change, works fine when I manually call Macros.define_alphabet like at the top, but not when I do it in any kind of comprehension or using Enum.each.
It's not a huge deal, but I'd like to be able to programmatically add to and remove from the list of alphabets depending on a user-defined configuration.
I'm sure as I get further into Metaprogramming Elixir, I'll be able to figure this out, but if anyone has any suggestions, I'd appreciate it.
"string"
or an:atom
, they are passed as literals, whereas when you pass variables, they are passed as how a variable is represented in Elixir AST:{:name, [line: 24], Elixir}
– davoclavo