Elixir macro expansion problems, but only in a comprehension

List comprehensions is a way to consume one list and get another list (or Enumerable in general case) from it. In your case you don't want to get a new list, you want to define functions in module. So, list comprehensions isn't appropriate way to do it.

You could use yet another macros to define alphabets from a map.


Figured it out. Works either way if I pass the bind_quoted list to quote, though I haven't found a way to pre-calculate the length and use :random.uniform like I was before, to avoid having to do the whole list conversion for every character choice.

# 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 any way 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 Better answer after 4 more years experience and reading Metaprogramming Elixir. I pre-split the alphabets using String.graphemes/1 and use Enum.random/1, the latter 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"]

Tags:

Macros

Elixir