defmodule Megadef do @moduledoc """ Utilities to make it easier to exract function parameters from keyword lists or maps. Elixir encourages you to use keyword lists for optional parameters. The problem is that oyu can't use pattern matching to extract their value, because users expect to be able to supply parameters in any order. You're forced to write something like this: def f(options) do a = Keyword.get(options, :a, 1) b = Keyword.get(options, :b, 2) c = Keyword.get(options, :c, 3) d = Keyword.get(options, :d, 4) # ... end For some (most?) use cases, it would be better if you could write something like this: def f([a: 1, b: 2, c: 3]) do #... end which would desuger into the above. This package adds some macros that make it possible. It defines 4 macros: * `Megadef.megadef/2` (expands into `Kernel.def/2`) * `Megadef.megadefp/2` (expands into `Kernel.defp/2`) * `Megadef.megadefmacro/2` (expands into `Kernel.defmacro/2`) * `Megadef.megadefmacrop/2` (expands into `Kernel.defmacrp/2`) For example: # The `= options` is required; we need an argument name for documentation purposes def f(&[a: 1, b: 2, c: 3] = options) do # ... end It will expand into: def f(options) do a = Keyword.get(options, :a, 1) b = Keyword.get(options, :b, 2) c = Keyword.get(options, :c, 3) # ... end ## Reference The `megadef` macro supports parameter extraction from maps or keyword lists. Both required and optional parameters are supported. Keyword list with optional parameters: megadef f(&[a: 1, b: 2] = options) do {a, b} end If all parameters in the keyword list are optional, you should make the list itself optional: megadef f(&[a: 1, b: 2] = options \\\\ []) do {a, b} end Keyword list with optional and required parameters: megadef f(&[a, b: 2] = options) do {a, b} end In this case, you haven't provided a default value for `a`. This means that `a` is a required parameter. The parameter will be fetched by `Keyword.fetch!/2` from the keyword list, and if it doesn't exist it will raise an error. ## Internals These macros use the unary `Kernel.&/1` operator. This operator has very low precedence, which means that it encloses the whole argument. For example: `&[a: 1, b: 2] = opts \\\\ []` is equivalent to `&([a: 1, b: 2] = opts \\\\ [])`. The semantics are backwards-compatible with the semantics inside argument lists of functions defined with `Kernel.def/2`, because you can't use `&...` in a pattern. If you want, you can redefine `def(macro)?(p)?` so that they call `mdegadef(macro)?(p)?`. You probably shouldn't, though, because it can make your code more confusing to read. """ # Public API @doc """ Same thing as `Kernel.def/2` but with extended support for matching arguments with keyword lists and optional values in maps. """ defmacro megadef(definition, body \\ []) do expand_megadef(:def, definition, body) end @doc """ Same thing as `Kernel.defp/2` but with extended support for matching arguments with keyword lists and optional values in maps. """ defmacro megadefp(definition, body \\ []) do expand_megadef(:defp, definition, body) end @doc """ Same thing as `Kernel.defmacro/2` but with extended support for matching arguments with keyword lists and optional values in maps. """ defmacro megadefmacro(definition, body \\ []) do expand_megadef(:defmacro, definition, body) end @doc """ Same thing as `Kernel.defmacrop/2` but with extended support for matching arguments with keyword lists and optional values in maps. """ defmacro megadefmacrop(definition, body \\ []) do expand_megadef(:defmacrop, definition, body) end # Macroexpansions # Utilities to generate accessors defp declaration_for_option_from_keyword_list(option_list, {option_name, default_value}) do quote do var!(unquote({option_name, [], nil})) = Keyword.get(unquote(option_list), unquote(option_name), unquote(default_value)) end end defp declaration_for_option_from_keyword_list(option_list, option_name) do {var_name, _, _} = option_name quote do var!(unquote({var_name, [], nil})) = Keyword.fetch!(unquote(option_list), unquote(var_name)) end end defp declaration_for_option_from_map(option_list, {option_name, default_value}) do quote do var!(unquote({option_name, [], nil})) = Map.get(unquote(option_list), unquote(option_name), unquote(default_value)) end end # Map defp expand_arg( {:&, _, [{:=, _, [{:%{}, _, options}, arg]} ]}) when is_list(options) do declarations = Enum.map(options, fn option -> declaration_for_option_from_map(arg, option) end) map_arg = quote(do: %{} = unquote(arg)) {map_arg, declarations} end defp expand_arg( {:&, _, [{:\\, _, [{:=, _, [{:%{}, _, options}, arg]}, default]} ]}) when is_list(options) do declarations = Enum.map(options, fn option -> declaration_for_option_from_map(arg, option) end) map_arg = quote(do: %{} = unquote(arg)) optional_arg = quote(do: unquote(map_arg) \\ unquote(default)) {optional_arg, declarations} end # Keyword list defp expand_arg( {:&, _, [{:=, _, [options, arg]} ]}) when is_list(options) do declarations = Enum.map(options, fn option -> declaration_for_option_from_keyword_list(arg, option) end) {arg, declarations} end defp expand_arg( {:&, _, [{:\\, _, [{:=, _, [options, arg]}, default]} ]}) when is_list(options) do declarations = Enum.map(options, fn option -> declaration_for_option_from_keyword_list(arg, option) end) optional_arg = quote(do: unquote(arg) \\ unquote(default)) {optional_arg, declarations} end # All other arguments are passed unchanged defp expand_arg(full_arg), do: {full_arg, []} defp expand_args(args) do {args, declaration_groups} = args |> Enum.map(&expand_arg/1) |> Enum.unzip() declarations = List.flatten(declaration_groups) {args, declarations} end defp extract_expressions([do: inner]), do: List.wrap(inner) defp expand_megadef(kind, definition, body) when is_atom(kind) do {fun, args} = Macro.decompose_call(definition) # Build a new argument list and the declarations necessary to extract # the optional parameters {argspec, declarations} = expand_args(args) # Make sure we can use the old function body body_expressions = extract_expressions(body) # Incorporate the declarations into the new body new_body = declarations ++ body_expressions # define the macro or function case kind do :def -> quote do def unquote(fun)(unquote_splicing(argspec)) do unquote_splicing(new_body) end end :defp -> quote do defp unquote(fun)(unquote_splicing(argspec)) do unquote_splicing(new_body) end end :defmacro -> quote do defmacro unquote(fun)(unquote_splicing(argspec)) do unquote_splicing(new_body) end end :defmacrop -> quote do defmacrop unquote(fun)(unquote_splicing(argspec)) do unquote_splicing(new_body) end end end end end