Current section
Files
Jump to
Current section
Files
lib/megadef.ex
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