defmodule Coerce do
@moduledoc """
Coerce allows defining coercions between data types.
These are standardized conversions of one kind of data to another.
A coercion can be defined using `defcoercion`.
The code that coercion is compiled to attempts to ensure that the result
is relatively fast (with the possibility for further optimization in the future).
Coerce does _not_ come with built-in coercions, instead allowing libraries that build on top of it
to define their own rules.
"""
@builtin_guards_list [
is_tuple: Tuple,
is_atom: Atom,
is_list: List,
is_map: Map,
is_bitstring: BitString,
is_integer: Integer,
is_float: Float,
is_function: Function,
is_pid: PID,
is_port: Port,
is_reference: Reference]
@doc """
Performs value coercion,
the simpler of the two values is converted into
a more complex type, and the result is returned as tuple.
## Examples
iex> require Coerce
iex> Coerce.defcoercion(Integer, Float) do
iex> def coerce(int, float) do
iex> {int + 0.0, float}
iex> end
iex> end
iex> Coerce.coerce(1, 2.3)
{1.0, 2.3}
iex> Coerce.coerce(1.4, 42)
{1.4, 42.0}
iex> require Coerce
iex> Coerce.defcoercion(BitString, Atom) do
iex> def coerce(str, atom) do
iex> {str, inspect(atom)}
iex> end
iex> end
iex> Coerce.coerce("foo", Bar)
{"foo", "Bar"}
iex> Coerce.coerce("baz", :qux)
{"baz", ":qux"}
"""
@spec coerce(a, b) :: {a, a} | {b, b} when a: any, b: any
def coerce(a, b)
def coerce(a = %a_mod{}, b = %a_mod{}) do
{a, b}
end
def coerce(a = %a_mod{}, b = %b_mod{}) do
Module.concat([Coerce.Implementations, a_mod, b_mod]).coerce(a, b)
end
for {guard, mod} <- @builtin_guards_list do
def coerce(a = %a_struct_mod{}, b) when unquote(guard)(b) do
Module.concat([Coerce.Implementations, a_struct_mod, unquote(mod)]).coerce(a, b)
end
def coerce(a, b = %b_struct_mod{}) when unquote(guard)(a) do
Module.concat([Coerce.Implementations, unquote(mod), b_struct_mod]).coerce(a, b)
end
end
for {guard_a, a_mod} <- @builtin_guards_list, {guard_b, b_mod} <- @builtin_guards_list do
if guard_a == guard_b do
def coerce(a, b) when unquote(guard_a)(a) and unquote(guard_a)(b) do
{a, b}
end
else
primary_module = Module.concat([Coerce.Implementations, a_mod, b_mod])
def coerce(a, b) when unquote(guard_a)(a) and unquote(guard_b)(b) do
# Uses Kernel.apply to mitigate warning if module does not exist...
apply(unquote(primary_module), :coerce, [a, b])
end
end
end
@doc """
Define a coercion between two data types.
Expects two module names as the first two arguments and a `do`-block as third argument.
A `Coerc.CompileError` will be raised at compile-time if the coercion macro is called improperly.
"""
defmacro defcoercion(first_module, second_module, [do: block]) do
first_module = Macro.expand_once(first_module, __CALLER__)
second_module = Macro.expand_once(second_module, __CALLER__)
unless is_atom(first_module) && is_atom(second_module) do
raise Coerce.CompileError, "`Coerce.defcoercion` called with improper arguments. Expects #{inspect(first_module)} and #{inspect(second_module)} to be module names."
end
primary_module = Module.concat([Coerce.Implementations, first_module, second_module])
secondary_module = Module.concat([Coerce.Implementations, second_module, first_module])
quote do
defmodule unquote(primary_module) do
@moduledoc false
unquote(block)
end
Code.ensure_compiled!(unquote(primary_module))
unless function_exported?(unquote(primary_module), :coerce, 2) do
raise Coerce.CompileError, "`Coerce.defcoercion` implementation does not implement `coerce/2`."
end
defmodule unquote(secondary_module) do
@moduledoc false
def coerce(lhs, rhs) do
{rhs, lhs} = unquote(primary_module).coerce(rhs, lhs)
{lhs, rhs}
end
end
end
end
end