Current section
Files
Jump to
Current section
Files
lib/tagged_tuple.ex
defmodule TaggedTuple do
@moduledoc "README.md"
|> File.read!()
|> String.split("[//]: # (Documentation)\n")
|> Enum.at(1)
|> String.trim("\n")
defmacro __using__(_opts) do
quote do
require unquote(__MODULE__)
import unquote(__MODULE__),
only: [
---: 2
]
end
end
@doc """
Defines a tagged tuple. It's equivalent to `{tag, value}`.
The operator is right-associative. When it's called several times in a row,
it assembles a tag chain with a value in the core.
:x --- :y --- :z --- "value" == {:x, {:y, {:z, "value"}}}
Can be used in Expressions and Pattern Matchings.
## Examples
iex> use TaggedTuple
...> :tag --- 12
{:tag, 12}
...> tagged_tuple = :a --- :tag --- :chain --- 12
{:a, {:tag, {:chain, 12}}}
...> match?(:a --- :tag --- _tail, tagged_tuple)
true
...> :a --- t1 --- t2 --- core_value = tagged_tuple
...> t1 == :tag
...> t2 == :chain
...> core_value == 12
"""
# credo:disable-for-next-line
defmacro tag --- value do
quote do
{unquote(tag), unquote(value)}
end
end
defguardp not_tuple(t1)
when not is_tuple(t1)
defguardp not_tuple(t2, t1)
when not is_tuple(t2) and not is_tuple(t1)
defguardp not_tuple(t3, t2, t1)
when not is_tuple(t3) and not is_tuple(t2) and not is_tuple(t1)
defguardp not_tuple(t4, t3, t2, t1)
when not is_tuple(t4) and not is_tuple(t3) and not is_tuple(t2) and not is_tuple(t1)
defguardp not_tuple(t5, t4, t3, t2, t1)
when not is_tuple(t5) and not is_tuple(t4) and not is_tuple(t3) and not is_tuple(t2) and
not is_tuple(t1)
@doc """
Returns a tagged tuple by attaching the tag chain to the core value.
`tag_chain` can be a nested tuple with tags returned from the `split/1`
or a list with tags.
## Example
iex> TaggedTuple.tag(2.5, :some_tag)
{:some_tag, 2.5}
iex> TaggedTuple.tag(7, {:a, {:tag, :chain}})
{:a, {:tag, {:chain, 7}}}
iex> TaggedTuple.tag(7, [:a, :tag, :chain])
{:a, {:tag, {:chain, 7}}}
"""
def tag(value, tag_chain)
def tag(v, []), do: v
def tag(v, [_ | _] = tag_chain) do
tag_chain
|> List.insert_at(-1, v)
|> from_list()
end
def tag(v, t1) when not_tuple(t1),
do: {t1, v}
def tag(v, {t2, t1}) when not_tuple(t2, t1),
do: {t2, {t1, v}}
def tag(v, {t3, {t2, t1}}) when not_tuple(t3, t2, t1),
do: {t3, {t2, {t1, v}}}
def tag(v, {t4, {t3, {t2, t1}}}) when not_tuple(t4, t3, t2, t1),
do: {t4, {t3, {t2, {t1, v}}}}
def tag(v, {t5, {t4, {t3, {t2, t1}}}}) when not_tuple(t5, t4, t3, t2, t1),
do: {t5, {t4, {t3, {t2, {t1, v}}}}}
def tag(v, tag_chain) when tuple_size(tag_chain) == 2 do
tag_chain
|> to_list()
|> List.insert_at(-1, v)
|> from_list()
end
@doc """
Removes the given subchain from the tag chain's beginning and return the
tagged tuple built from the rest. When the full tag chain is given,
it returns the core value.
Raises `ArgumentError` exception if the passed tag subchain doesn't match
the begginning of the tag chain of the tagged tuple.
`tag_chain` can be a nested tuple with tags returned from the `split/1`
or a list with tags.
## Examples
iex> value = {:a, {:tag, {:chain, 2}}}
iex> TaggedTuple.untag!(value, :a)
{:tag, {:chain, 2}}
iex> TaggedTuple.untag!(value, {:a, :tag})
{:chain, 2}
iex> TaggedTuple.untag!(value, {:a, {:tag, :chain}})
2
iex> TaggedTuple.untag!(value, [:a, :tag, :chain])
2
iex> value = {:other, {:stuff, 2}}
...> TaggedTuple.untag!(value, {:a, {:tag, :chain}})
** (ArgumentError) Tag chain {:a, {:tag, :chain}} doesn't match one in the tagged tuple {:other, {:stuff, 2}}.
"""
def untag!(tagged_tuple, tag_chain) do
case untag(tagged_tuple, tag_chain) do
{:ok, value} ->
value
{:error, :mismatch} ->
Kernel.raise(ArgumentError, """
Tag chain #{inspect(tag_chain)} doesn't match one in \
the tagged tuple #{inspect(tagged_tuple)}.\
""")
end
end
@doc """
Same as `untag!/2`.
Returns `{:error, :mismatch}` when `tag_chain` doesn't match
the beginning part of the tag chain of the tagged tuple.
"""
def untag(tagged_tuple, tag_chain)
def untag({t1, value}, [t1]) do
{:ok, value}
end
def untag({t1, tail}, [t1 | rest]) do
untag(tail, rest)
end
def untag({t1, value}, t1) do
{:ok, value}
end
def untag({t1, tail}, {t1, rest}) do
untag(tail, rest)
end
def untag(_, _) do
{:error, :mismatch}
end
@doc """
Returns the tag chain and the core value from the given tagged tuple.
## Examples
iex> {chain, value} = TaggedTuple.split({:a, {:tag, {:chain, 2}}})
...> chain == {:a, {:tag, :chain}}
...> value == 2
iex> TaggedTuple.tag(value, chain)
{:a, {:tag, {:chain, 2}}}
"""
def split(tagged_tuple) when is_tuple(tagged_tuple) do
list = to_list(tagged_tuple)
value = List.last(list)
tags =
case List.delete_at(list, -1) do
[one] -> one
[_ | _] = many -> from_list(many)
end
{tags, value}
end
@doc """
Converts a tagged tuple to a list.
The `tag_fun` will be invoked with each tag, and the result will be inserted
into the list. The `value_fun` will be invoked with the tagged tuple's
core value, and the result will be inserted into the list.
## Examples
iex> TaggedTuple.to_list({:a, {:tag, {:chain, 2}}})
[:a, :tag, :chain, 2]
iex> TaggedTuple.to_list({:a, {:tag, {:chain, 2}}}, &to_string/1, &(&1 * 100))
["a", "tag", "chain", 200]
"""
def to_list(tagged_tuple, tag_fun \\ fn x -> x end, value_fun \\ fn y -> y end) do
do_to_list(tagged_tuple, tag_fun, value_fun, [])
end
defp do_to_list({head, tail}, tag_fun, value_fun, acc),
do: do_to_list(tail, tag_fun, value_fun, [tag_fun.(head) | acc])
defp do_to_list(value, _tag_fun, value_fun, acc), do: Enum.reverse([value_fun.(value) | acc])
@doc """
Converts a list to a tagged tuple.
The list must have at least two elements, a tag and a value, respectfully.
The `tag_fun` will be invoked with each tag, and the result will be added
to the tuple. The `value_fun` will be invoked with the core value, and
the result will be added to the tuple.
## Examples
iex> TaggedTuple.from_list([:a, :tag, :chain, 2])
{:a, {:tag, {:chain, 2}}}
iex> TaggedTuple.from_list(["a", "tag", "chain", 200], &String.to_existing_atom/1, &div(&1, 100))
{:a, {:tag, {:chain, 2}}}
"""
def from_list(list, tag_fun \\ fn x -> x end, value_fun \\ fn y -> y end)
def from_list([tag], tag_fun, _value_fun) do
tag_fun.(tag)
end
def from_list(list, tag_fun, value_fun) do
[value, tag | tail] = Enum.reverse(list)
do_from_list(tail, tag_fun, {tag_fun.(tag), value_fun.(value)})
end
defp do_from_list([tag | tail], tag_fun, acc) do
do_from_list(tail, tag_fun, {tag_fun.(tag), acc})
end
defp do_from_list([], _tag_fun, acc) do
acc
end
@doc """
Converts a tagged tuple to a map.
The returned map can be encoded into a JSON to transmit over the network
or persist in a database.
The `tag_fun` will be invoked with each tag, and the result will be inserted
into the map. The `value_fun` will be invoked with the tagged tuple's
core value, and the result will be inserted into the map.
## Examples
iex> TaggedTuple.to_map({:a, {:tag, {:chain, 2}}})
%{a: %{tag: %{chain: 2}}}
iex> TaggedTuple.to_map({:a, {:tag, {:chain, 2}}}, &to_string/1, &(&1 * 100))
%{"a" => %{"tag" => %{"chain" => 200}}}
"""
def to_map(tagged_tuple, tag_fun \\ fn x -> x end, value_fun \\ fn y -> y end)
when is_tuple(tagged_tuple) do
[value, tag | tail] =
tagged_tuple
|> to_list()
|> Enum.reverse()
do_to_map(tail, tag_fun, %{tag_fun.(tag) => value_fun.(value)})
end
defp do_to_map([tag | tail], tag_fun, acc),
do: do_to_map(tail, tag_fun, %{tag_fun.(tag) => acc})
defp do_to_map([], _tag_fun, acc), do: acc
@doc """
Converts a map to a tagged tuple.
The `tag_fun` will be invoked with each tag, and the result will be added
to the tuple. The `value_fun` will be invoked with the core value,
and the result will be added to the tuple.
## Examples
iex> TaggedTuple.from_map(%{a: %{tag: %{chain: 2}}})
{:a, {:tag, {:chain, 2}}}
iex> TaggedTuple.from_map(%{"a" => %{"tag" => %{"chain" => 20}}}, &String.to_existing_atom/1, &div(&1, 10))
{:a, {:tag, {:chain, 2}}}
"""
def from_map(map, tag_fun \\ fn x -> x end, value_fun \\ fn y -> y end) do
do_from_map(map, tag_fun, value_fun, [])
end
defp do_from_map(%{} = map, tag_fun, value_fun, acc) do
{tag1, tail} = Enum.at(map, 0)
do_from_map(tail, tag_fun, value_fun, [tag_fun.(tag1) | acc])
end
defp do_from_map(value, _tag_fun, value_fun, acc) do
[value_fun.(value) | acc]
|> Enum.reverse()
|> from_list()
end
end