import TypeClass
defclass Witchcraft.Functor do
@moduledoc ~S"""
Functors are datatypes that allow the application of functions to their interior values.
Always returns data in the same structure (same size, tree layout, and so on).
Please note that bitstrings are not functors, as they fail the
functor composition constraint. They change the structure of the underlying data,
and thus composed lifting does not equal lifing a composed function. If you
need to map over a bitstring, convert it to and from a charlist.
## Type Class
An instance of `Witchcraft.Functor` must define `Witchcraft.Functor.map/2`.
Functor [map/2]
"""
alias __MODULE__
use Witchcraft.Internal
use Quark
@type t :: any()
where do
@doc ~S"""
`map` a function into one layer of a data wrapper.
There is an autocurrying variant: `lift/2`.
## Examples
iex> map([1, 2, 3], fn x -> x + 1 end)
[2, 3, 4]
iex> %{a: 1, b: 2} ~> fn x -> x * 10 end
%{a: 10, b: 20}
iex> map(%{a: 2, b: [1, 2, 3]}, fn
...> int when is_integer(int) -> int * 100
...> value -> inspect(value)
...> end)
%{a: 200, b: "[1, 2, 3]"}
"""
@spec map(Functor.t(), (any() -> any())) :: Functor.t()
def map(wrapped, fun)
end
properties do
def identity(data) do
wrapped = generate(data)
wrapped
|> Functor.map(&id/1)
|> equal?(wrapped)
end
def composition(data) do
wrapped = generate(data)
f = fn x -> inspect(wrapped == x) end
g = fn x -> inspect(wrapped != x) end
left = Functor.map(wrapped, fn x -> x |> g.() |> f.() end)
right = wrapped |> Functor.map(g) |> Functor.map(f)
equal?(left, right)
end
end
@doc ~S"""
`map` with its arguments flipped.
## Examples
iex> across(fn x -> x + 1 end, [1, 2, 3])
[2, 3, 4]
iex> fn
...> int when is_integer(int) -> int * 100
...> value -> inspect(value)
...> end
...> |> across(%{a: 2, b: [1, 2, 3]})
%{a: 200, b: "[1, 2, 3]"}
"""
@spec across((any() -> any()), Functor.t()) :: Functor.t()
def across(fun, wrapped), do: map(wrapped, fun)
@doc ~S"""
`map/2` but with the function automatically curried
## Examples
iex> lift([1, 2, 3], fn x -> x + 1 end)
[2, 3, 4]
iex> [1, 2, 3]
...> |> lift(fn x -> x + 55 end)
...> |> lift(fn y -> y * 10 end)
[560, 570, 580]
iex> [1, 2, 3]
...> |> lift(fn(x, y) -> x + y end)
...> |> List.first()
...> |> apply([9])
10
"""
@spec lift(Functor.t(), fun()) :: Functor.t()
def lift(wrapped, fun), do: Functor.map(wrapped, curry(fun))
@doc """
`lift/2` but with arguments flipped.
## Examples
iex> fn x -> x + 1 end |> over([1, 2, 3])
[2, 3, 4]
"""
@spec over(fun(), Functor.t()) :: Functor.t()
def over(fun, wrapped), do: lift(wrapped, fun)
@doc ~S"""
Operator alias for `lift/2`
## Example
iex> [1, 2, 3]
...> ~> fn x -> x + 55 end
...> ~> fn y -> y * 10 end
[560, 570, 580]
iex> [1, 2, 3]
...> ~> fn(x, y) -> x + y end
...> |> List.first()
...> |> apply([9])
10
"""
defalias data ~> fun, as: :lift
@doc ~S"""
`~>/2` with arguments flipped.
iex> (fn x -> x + 5 end) <~ [1,2,3]
[6, 7, 8]
Note that the mnemonic is flipped from `|>`, and combinging directions can
be confusing. It's generally recommended to use `~>`, or to keep `<~` on
the same line both of it's arguments:
iex> fn(x, y) -> x + y end <~ [1, 2, 3]
...> |> List.first()
...> |> apply([9])
10
...or in an expression that's only pointing left:
iex> fn y -> y * 10 end
...> <~ fn x -> x + 55 end
...> <~ [1, 2, 3]
[560, 570, 580]
"""
def fun <~ data, do: data ~> fun
@doc ~S"""
Replace all inner elements with a constant value
## Examples
iex> replace([1, 2, 3], "hi")
["hi", "hi", "hi"]
"""
@spec replace(Functor.t(), any()) :: Functor.t()
def replace(wrapped, replace_with), do: wrapped ~> (&constant(replace_with, &1))
@doc """
`map` a function over a data structure, with each mapping occuring asynchronously.
Especially helpful when each application take a long time.
## Examples
iex> async_map([1, 2, 3], fn x -> x * 10 end)
[10, 20, 30]
0..10_000
|> Enum.to_list()
|> async_map(fn x ->
Process.sleep(500)
x * 10
end)
#=> [0, 10, ...] in around a second
"""
@spec async_map(Functor.t(), (any() -> any())) :: Functor.t()
def async_map(functor, fun) do
functor
|> Functor.map(fn item ->
Task.async(fn ->
fun.(item)
end)
end)
|> Functor.map(&Task.await/1)
end
@doc """
`async_map/2` with arguments flipped.
## Examples
iex> fn x -> x * 10 end
...> |> async_across([1, 2, 3])
[10, 20, 30]
fn x ->
Process.sleep(500)
x * 10
end
|> async_across(Enumto_list(0..10_000))
#=> [0, 10, ...] in around a second
"""
@spec async_across((any() -> any()), Functor.t()) :: Functor.t()
def async_across(fun, functor), do: async_map(functor, fun)
@doc """
The same as `async_map/2`, except with the mapping function curried
## Examples
iex> async_lift([1, 2, 3], fn x -> x * 10 end)
[10, 20, 30]
0..10_000
|> Enum.to_list()
|> async_lift(fn x ->
Process.sleep(500)
x * 10
end)
#=> [0, 10, ...] in around a second
"""
@spec async_lift(Functor.t(), fun()) :: Functor.t()
def async_lift(functor, fun), do: async_map(functor, curry(fun))
@doc """
`async_lift/2` with arguments flipped.
## Examples
iex> fn x -> x * 10 end
...> |> async_over([1, 2, 3])
[10, 20, 30]
fn x ->
Process.sleep(500)
x * 10
end
|> async_over(Enumto_list(0..10_000))
#=> [0, 10, ...] in around a second
"""
@spec async_over(fun(), Functor.t()) :: Functor.t()
def async_over(fun, functor), do: async_map(functor, fun)
end
definst Witchcraft.Functor, for: Function do
use Quark
@doc """
Compose functions
## Example
iex> ex = Witchcraft.Functor.lift(fn x -> x * 10 end, fn x -> x + 2 end)
...> ex.(2)
22
"""
def map(f, g), do: Quark.compose(g, f)
end
definst Witchcraft.Functor, for: List do
def map(list, fun), do: Enum.map(list, fun)
end
definst Witchcraft.Functor, for: Tuple do
def map(tuple, fun) do
case tuple do
{} ->
{}
{first} ->
{fun.(first)}
{first, second} ->
{first, fun.(second)}
{first, second, third} ->
{first, second, fun.(third)}
{first, second, third, fourth} ->
{first, second, third, fun.(fourth)}
{first, second, third, fourth, fifth} ->
{first, second, third, fourth, fun.(fifth)}
big_tuple ->
last_index = tuple_size(big_tuple) - 1
mapped =
big_tuple
|> elem(last_index)
|> fun.()
put_elem(big_tuple, last_index, mapped)
end
end
end
definst Witchcraft.Functor, for: Map do
def map(hashmap, fun) do
hashmap
|> Map.to_list()
|> Witchcraft.Functor.map(fn {key, value} -> {key, fun.(value)} end)
|> Enum.into(%{})
end
end