diff --git a/README.md b/README.md index 6b7f705..28403a0 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,6 @@ Functors, monads, arrows, and categories ``` def deps do - [{:witchcraft, "~> 0.2.0"}] + [{:witchcraft, "~> 0.3.0"}] end ``` diff --git a/lib/witchcraft.ex b/lib/witchcraft.ex deleted file mode 100644 index 6da8b2c..0000000 --- a/lib/witchcraft.ex +++ /dev/null @@ -1,25 +0,0 @@ -defmodule Witchcraft do - # import Witchcraft.Monoid - # import Witchcraft.Monoid.Functions - # import Witchcraft.Monoid.Properties - - # import Witchcraft.Functor - # import Witchcraft.Functor.Functions - # import Witchcraft.Functor.Properties - - # import Witchcraft.Applicative - # import Witchcraft.Applicative.Functions - # import Witchcraft.Applicative.Properties - - # import Witchcraft.Monad - # import Witchcraft.Monad.Functions - # import Witchcraft.Monad.Properties - - # import Witchcraft.Catgegory - # import Witchcraft.Catgegory.Functions - # import Witchcraft.Catgegory.Properties - - # import Witchcraft.Arrow - # import Witchcraft.Arrow.Functions - # import Witchcraft.Arrow.Properties -end diff --git a/lib/witchcraft/adt/maybe.ex b/lib/witchcraft/adt/maybe.ex deleted file mode 100644 index df99b7d..0000000 --- a/lib/witchcraft/adt/maybe.ex +++ /dev/null @@ -1,48 +0,0 @@ -defmodule ADT.Maybe do - @moduledoc ~S""" - `Maybe` encapulates the idea of a value that might not be there. - This is often a failed computation, but in some cases an empty value may be the - expected behaviour. - - A `%Maybe{}` value can either be `Just some_value` or `Nothing`. More typical of - Elixir is to use `{:ok, some_value}`, or `{:error, some_reason}`. By contrast, - `%Maybe{}` has an implied `:ok`, if there is a value in the `maybe` key. - - Please note that this approach does not track error reasons, as a `Nothing` value - may not be an error. If you are looking for error tracking behaviour, consider - `%MaybePlus{}` - """ - - defstruct maybe: nil - - # @type nothing :: :nothing - # @type just(a) :: any - # @type maybe(a) :: just(a) | nothing - - # @type maybe(a) :: %ADT.Maybe{maybe: a} - # @type maybe(a) :: any | :nothing - - @doc "Common useage" - # @spec from_status_tuple({atom, any}) :: maybe(any) - def from_status_tuple({:ok, payload}), do: %ADT.Maybe{maybe: payload} - def from_status_tuple({:error, reason}), do: %ADT.Maybe{maybe: nil} - - # @spec just?(maybe(any)) :: boolean - def just?(%ADT.Maybe{maybe: maybe}), do: !!maybe - - # @spec nothing?(maybe(any)) :: boolean - def nothing?(x), do: not just?(x) -end - -defimpl String.Chars, for: ADT.Maybe do - def to_string(%ADT.Maybe{maybe: maybe}) do - case maybe do - nil -> "Nothing" - x -> "Just #{ x }" - end - end -end - -defimpl Inspect, for: ADT.Maybe do - def inspect(t, _), do: "#" -end diff --git a/lib/witchcraft/adt/maybe_plus.ex b/lib/witchcraft/adt/maybe_plus.ex deleted file mode 100644 index 5fe474b..0000000 --- a/lib/witchcraft/adt/maybe_plus.ex +++ /dev/null @@ -1,50 +0,0 @@ -defmodule ADT.MaybePlus do - @moduledoc ~S""" - `MaybePlus` encapulates the idea of a value that might not be there. - This is often a failed computation, but in some cases an empty value may be - the expected behaviour. - - Much like `%Maybe{}`, a `%MaybePlus{}` value can either be `Just some_value`, - or `Nothing`, but with error reason tracking intact. - - More typical of Elixir is to use `{:ok, some_value}`, or `{:error, some_reason}`. - By contrast, `%MaybePlus{}` has an implied `:ok`, if there is a value in the - `maybe` key. - - The additional `meta` key exists to track error reasons, but may be used - for general purpose metadata (including success messaging). This has the - potential to enter an inconsitent state, where the `maybe` and `meta` values - come out of sync. It is recommended to *always* set both values when updating - the struct. - """ - - defstruct maybe: nil, meta: nil - - @type maybe_plus(a, b) :: %ADT.MaybePlus{maybe: a, meta: b} - - @spec from_status_tuple({atom, any}) :: maybe_plus(any, any) - def from_status_tuple({:ok, payload}), do: %ADT.MaybePlus{maybe: payload} - def from_status_tuple({:error, reason}), do: %ADT.MaybePlus{meta: reason} - - @spec just?(maybe_plus(any, any)) :: boolean - def just?(%ADT.MaybePlus{maybe: maybe}), do: !!maybe - - @spec nothing?(maybe_plus(any, any)) :: boolean - def nothing?(x), do: not just?(x) - - def meta(%ADT.MaybePlus{meta: meta}), do: meta -end - -defimpl String.Chars, for: ADT.MaybePlus do - def to_string(%ADT.MaybePlus{maybe: maybe, meta: meta}) do - clean_meta = meta || 'nil' - case maybe do - nil -> "{Nothing, meta: #{ clean_meta }}" - x -> "{Just #{ x }, meta: #{ clean_meta }}" - end - end -end - -defimpl Inspect, for: ADT.MaybePlus do - def inspect(t, _), do: "#" -end diff --git a/lib/witchcraft/applicative.ex b/lib/witchcraft/applicative.ex new file mode 100644 index 0000000..9a56fd4 --- /dev/null +++ b/lib/witchcraft/applicative.ex @@ -0,0 +1,146 @@ +defprotocol Witchcraft.Applicative do + @moduledoc """ + Applicative functors provide a method of applying a function contained in a + data structure to a value of the same type. This allows you to apply and compose + functions to values while avoiding repeated manual wrapping and unwrapping + of those values. + + # Properties + ## Identity + `apply`ing a lifted `id` to some lifted value `v` does not change `v` + + `apply(v, wrap(&id(&1))) == v` + + ## Composition + `apply` composes normally. + + `apply((wrap &compose(&1,&2)), (apply(u,(apply(v, w))))) == apply(u,(apply(v, w)))` + + ## Homomorphism + `apply`ing a `wrap`ped function to a `wrap`ped value is the same as wrapping the + result of the function on that value. + + `apply(wrap x, wrap f) == wrap f(x))` + + ## Interchange + The order does not matter when `apply`ing to a `wrap`ped value + and a `wrap`ped function. + + `apply(wrap y, u) == apply(u, wrap &(lift(y, &1))` + + ## Functor + Being an applicative _functor_, `apply` behaves as `lift` on `wrap`ped values + + `lift(x, f) == apply(x, (wrap f))` + + # Notes + Given that Elixir functons are right-associative, you can write clean looking, + but much more ambiguous versions: + + `wrap(y) |> apply(u) == apply(u, wrap(&lift(y, &1)))` + + `lift(x, f) == apply(x, wrap f)` + + However, it is strongly recommended to include the parentheses for clarity. + + """ + + @fallback_to_any true + + @doc ~S""" + Lift a pure value into a type provided by some specemin (usually the zeroth + or empty value of that type, but not nessesarily). + """ + @spec wrap(any, any) :: any + def wrap(specimen, bare) + + @doc ~S""" + Sequentially apply lifted function(s) to lifted data. + """ + @spec apply(any, (... -> any)) :: any + def apply(wrapped_value, wrapped_function) +end + +defimpl Witchcraft.Applicative, for: Any do + @doc ~S""" + By default, use the true identity functor (ie: don't wrap) + """ + def wrap(_, bare_value), do: bare_value + + @doc ~S""" + For un`wrap`ped values, treat `apply` as plain function application. + """ + def apply(bare_value, bare_function), do: Quark.Curry.curry(bare_function).(bare_value) +end + +defimpl Witchcraft.Applicative, for: List do + import Quark.Curry, only: [curry: 1] + + @doc ~S""" + + ```elixir + + iex> wrap([], 0) + [0] + + ``` + + """ + def wrap(_, bare), do: [bare] + + @doc ~S""" + + ```elixir + + iex> import Kernel, except: [apply: 2] + iex> apply([1,2,3], [&(&1 + 1), &(&1 * 10)]) + [2,3,4,10,20,30] + + iex> import Kernel, except: [apply: 2] + iex> import Witchcraft.Functor, only: [lift: 2] + iex> apply([9,10,11], lift([1,2,3], &(fn x -> x * &1 end))) + [9,10,11,18,20,22,27,30,33] + + ``` + + """ + def apply(_, []), do: [] + def apply(values, [fun|funs]) do + Enum.map(values, curry(fun)) ++ Witchcraft.Applicative.apply(values, funs) + end +end + +defimpl Witchcraft.Applicative, for: Witchcraft.Id do + import Quark.Curry, only: [curry: 1] + alias Witchcraft.Id, as: Id + + @doc ~S""" + + ```elixir + + iex> %Witchcraft.Id{} |> wrap(9) + %Witchcraft.Id{id: 9} + + ``` + + """ + def wrap(_, bare), do: %Witchcraft.Id{id: bare} + + @doc ~S""" + ```elixir + + iex> import Kernel, except: [apply: 2] + iex> apply(%Witchcraft.Id{id: 42}, %Witchcraft.Id{id: &(&1 + 1)}) + %Witchcraft.Id{id: 43} + + iex> import Kernel, except: [apply: 2] + iex> import Witchcraft.Functor, only: [lift: 2] + iex> alias Witchcraft.Id, as: Id + iex> apply(%Id{id: 9}, lift(%Id{id: 2}, &(fn x -> x + &1 end))) + %Witchcraft.Id{id: 11} + + ``` + + """ + def apply(%Id{id: value}, %Id{id: fun}), do: %Id{id: curry(fun).(value)} +end diff --git a/lib/witchcraft/applicative/function.ex b/lib/witchcraft/applicative/function.ex new file mode 100644 index 0000000..39af1de --- /dev/null +++ b/lib/witchcraft/applicative/function.ex @@ -0,0 +1,46 @@ +defmodule Witchcraft.Applicative.Function do + @moduledoc ~S""" + Function helpers, derivatives and operators for `Witchcraft.Applicative` + """ + + import Kernel, except: [apply: 2] + + import Quark, only: [id: 1, flip: 1, constant: 2] + import Quark.Curry, only: [curry: 1] + + import Witchcraft.Applicative, only: [apply: 2] + import Witchcraft.Functor.Operator, only: [<~: 2, ~>: 2] + + @doc ~S""" + `lift` a function that takes a list of arguments + + ```elixir + + iex> lift([[1,2,3], [4,5,6]], &(&1 + &2)) + [5,6,7,6,7,8,7,8,9] + + iex> lift([[1,2], [3,4], [5,6]], &(&1 + &2 + &3)) + [9,10,10,11,10,11,11,12] + + iex> lift([[1,2], [3,4], [5,6], [7,8]], &(&1 + &2 + &3 + &4)) + [16,17,17,18,17,18,18,19,17,18,18,19,18,19,19,20] + + ``` + + """ + @spec lift(any, (... -> any)) :: any + def lift([value], fun), do: value ~> curry(fun) + def lift([head|tail], fun), do: Enum.reduce(tail, lift([head], fun), &apply/2) + + @doc ~S""" + Sequentially `apply`, and discard the second value of each pair. + """ + @spec seq_first([any]) :: any + def seq_first([a,b]), do: lift([a,b], &constant/2) + + @doc ~S""" + Sequentially `apply`, and discard the first value of each pair. + """ + @spec seq_second([any]) :: any + def seq_second([a,b]), do: lift([a,b], fn x -> constant(x, &id/1) end) +end diff --git a/lib/witchcraft/applicative/operator.ex b/lib/witchcraft/applicative/operator.ex new file mode 100644 index 0000000..af96278 --- /dev/null +++ b/lib/witchcraft/applicative/operator.ex @@ -0,0 +1,50 @@ +defmodule Witchcraft.Applicative.Operator do + @moduledoc ~S""" + """ + + import Kernel, except: [apply: 2] + import Witchcraft.Applicative, only: [apply: 2] + import Witchcraft.Applicative.Function, only: [lift: 2] + + @doc ~S""" + Infix alias for `Witchcraft.Applicative.apply`. If chaining, be sure to wrap + each layer in parentheses, as `~>>` and `~>` are left associative. + + ```elixir + + iex> [1,2,3] ~>> [&(&1 + 1), &(&1 * 10)] + [2,3,4,10,20,30] + + # iex> [9, 10] ~>> (Witchcraft.Applicative.Function.lift [1,2,3], &(fn x -> x + &1 end)) + iex> [9, 10] ~>> ([1,2,3] ~> &(fn x -> x * &1 end)) + [9, 10, 18, 20, 27, 30] + + ``` + + """ + @spec any ~>> any :: any + def value ~>> func, do: apply(value, func) + + @doc ~S""" + Infix alias for `Witchcraft.Applicative.apply`, with arguments reversed. + + This version is preferred, as it makes chaining arguments along wrapped + partial applications clearer when reading left-to-right. + + ```elixir + + iex> [&(&1 + 1), &(&1 * 10)] <<~ [1,2,3] + [2,3,4,10,20,30] + + iex> (&(fn x -> x * &1 end)) <~ [1,2,3] <<~ [9,10,11] + [9,10,11,18,20,22,27,30,33] + + ``` + + """ + @spec any <<~ any :: any + def func <<~ value, do: value ~>> func + + defdelegate functor_value ~> bare_function, to: Witchcraft.Functor, as: :lift + def bare_function <~ functor_value, do: functor_value ~> bare_function +end diff --git a/lib/witchcraft/applicative/property.ex b/lib/witchcraft/applicative/property.ex new file mode 100644 index 0000000..77f43e0 --- /dev/null +++ b/lib/witchcraft/applicative/property.ex @@ -0,0 +1,101 @@ +defmodule Witchcraft.Applicative.Property do + @moduledoc ~S""" + Check samples of your applicative functor to confirm that your data adheres to the + applicative properties. *All members* of your datatype should adhere to these rules, + *plus* implement `Witchcraft.Functor`. + + They are placed here as a quick way to spotcheck some of your values. + """ + + import Kernel, except: [apply: 2] + + import Quark, only: [compose: 2, id: 1] + import Quark.Curry, only: [curry: 1] + + import Witchcraft.Applicative, only: [apply: 2, wrap: 2] + import Witchcraft.Applicative.Operator, only: [~>: 2, <~: 2, ~>>: 2, <<~: 2] + + @doc ~S""" + `apply`ing a lifted `id` to some lifted value `v` does not change `v` + + ```elixir + + iex> spotcheck_identity [] + true + + iex> spotcheck_identity %Witchcraft.Id{} + true + + ``` + + """ + @spec spotcheck_identity(any) :: boolean + def spotcheck_identity(value), do: (value ~>> wrap(value, &id/1)) == value + + @doc ~S""" + `apply` composes normally. + + iex> spotcheck_composition([1, 2], [&(&1 * 2)], [&(&1 * 10)]) + true + + """ + @spec spotcheck_composition(any, any, any) :: boolean + def spotcheck_composition(value, fun1, fun2) do + wrap(value, &compose/2) <<~ fun1 <<~ fun2 <<~ value == fun1 <<~ (fun2 <<~ value) + end + + @doc ~S""" + `apply`ing a `wrap`ped function to a `wrap`ped value is the same as wrapping the + result of the function on that value. + + ```elixir + + iex> spotcheck_homomorphism([], 1, &(&1 * 10)) + true + + ``` + """ + @spec spotcheck_homomorphism(any, any, fun) :: boolean + def spotcheck_homomorphism(specemin, val, fun) do + curried = curry(fun) + wrap(specemin, val) ~>> wrap(specemin, curried) == wrap(specemin, curried.(val)) + end + + @doc ~S""" + The order does not matter when `apply`ing to a `wrap`ped value + and a `wrap`ped function. + + ```elixir + + iex> spotcheck_interchange(1, [&(&1 * 10)]) + true + + ``` + + """ + @spec spotcheck_interchange(any, any) :: boolean + def spotcheck_interchange(bare_val, wrapped_fun) do + wrap(wrapped_fun, bare_val) ~>> wrapped_fun + == wrapped_fun ~>> wrap(wrapped_fun, &(bare_val |> curry(&1).())) + end + + @doc ~S""" + + Being an applicative _functor_, `apply` behaves as `lift` on `wrap`ped values + + ```elixir + + iex> spotcheck_functor([1,2,3], &(&1 * 10)) + true + + iex> spotcheck_functor(%Witchcraft.Id{id: 7}, &(&1 * 99)) + true + + ``` + + """ + @spec spotcheck_functor(any, fun) :: boolean + def spotcheck_functor(wrapped_value, fun) do + wrapped_value ~> fun == wrapped_value ~>> wrap(wrapped_value, fun) + end +end diff --git a/lib/witchcraft/functor.ex b/lib/witchcraft/functor.ex index 833d880..b6fd6fd 100644 --- a/lib/witchcraft/functor.ex +++ b/lib/witchcraft/functor.ex @@ -1,5 +1,4 @@ defprotocol Witchcraft.Functor do - require Witchcraft.Utility.Id @moduledoc ~S""" Functors provide a way to apply a function to value(s) a datatype (lists, trees, maybes, etc). @@ -21,10 +20,10 @@ defprotocol Witchcraft.Functor do # Properties ## Identity Mapping the identity function over the object returns the same object - ex. `lift([1,2,3], &(&1)) == [1,2,3]` + ex. `lift([1,2,3], id) == [1,2,3]` ## Distributive - `lift(data, (f |> g)) == data |> lift f |> lift g` + `lift(data, (f |> g)) == data |> lift(f) |> lift(g)` ## Associates all objects Mapping a function onto an object returns a value. @@ -39,22 +38,17 @@ defprotocol Witchcraft.Functor do # Examples - ``` - - iex> Witchcraft.Functor.lift([1,2,3], &(&1 + 1)) - [2,3,4] + iex> [1,2,3] |> lift(&(&1 + 1)) + [2,3,4] - iex> defimpl Witchcraft.Functor, for: Witchcraft.Utility.Id do - iex> def lift(%Witchcraft.Utility.Id{id: inner}, func), do: %Witchcraft.Utility.Id{id: func.(inner)} - iex> end - iex> Witchcraft.Functor.lift(%Witchcraft.Utility.Id{id: 1}, &(&1 + 1)) - %Witchcraft.Utility.Id{id: 2} + iex> defimpl Witchcraft.Functor, for: Witchcraft.Id do + iex> def lift(%Witchcraft.Id{id: inner}, func), do: %Witchcraft.Id{id: func.(inner)} + iex> end + iex> lift(%Witchcraft.Id{id: 1}, &(&1 + 1)) + %Witchcraft.Id{id: 2} - ``` """ - @fallback_to_any true - @doc """ Apply a function to every element in some collection, tree, or other structure. The collection will retain its structure (list, tree, and so on). @@ -63,12 +57,30 @@ defprotocol Witchcraft.Functor do def lift(data, function) end -defimpl Witchcraft.Functor, for: Any do - @doc "Default implementation of `Functor` is `Enum.map`" +defimpl Witchcraft.Functor, for: List do + @doc ~S""" + + ```elixir + + iex> lift([1,2,3], &(&1 + 1)) + [2,3,4] + + ``` + + """ def lift(data, func), do: Enum.map(data, func) end -defimpl Witchcraft.Functor, for: Witchcraft.Utility.Id do - @doc "Example struct implimentation" - def lift(%Witchcraft.Utility.Id{id: data}, func), do: %Witchcraft.Utility.Id{id: func.(data)} +defimpl Witchcraft.Functor, for: Witchcraft.Id do + @doc ~S""" + + ```elixir + + iex> lift(%Witchcraft.Id{id: 5}, &(&1 * 101)) + %Witchcraft.Id{id: 505} + + ``` + + """ + def lift(%Witchcraft.Id{id: data}, func), do: %Witchcraft.Id{id: func.(data)} end diff --git a/lib/witchcraft/functor/function.ex b/lib/witchcraft/functor/function.ex new file mode 100644 index 0000000..f5c9081 --- /dev/null +++ b/lib/witchcraft/functor/function.ex @@ -0,0 +1,42 @@ +defmodule Witchcraft.Functor.Function do + @moduledoc ~S""" + Functions that come directly from `lift`. + """ + + use Quark.Partial + import Witchcraft.Functor, only: [lift: 2] + + @doc ~S""" + Not strictly a curried version of `lift/2`. `lift/1` partially applies a function, + to create a "lifted" version of that function. + + ```elixir + + iex> x10 = &lift(fn x -> x * 10 end).(&1) + iex> [1,2,3] |> x10.() + [10,20,30] + + iex> x10 = &lift(fn x -> x * 10 end).(&1) + iex> %Witchcraft.Id{id: 13} |> x10.() + %Witchcraft.Id{id: 130} + + ``` + + """ + @spec lift((... -> any)) :: (any -> any) + defpartial lift(fun), do: &lift(&1, fun) + + @doc ~S""" + Replace all of the input's data nodes with some constant value + + ```elixir + + iex> [1,2,3] |> replace(42) + [42, 42, 42] + + ``` + + """ + @spec replace(any, any) :: any + def replace(functor, const), do: lift(functor, &Quark.constant(const, &1)) +end diff --git a/lib/witchcraft/functor/functions.ex b/lib/witchcraft/functor/functions.ex deleted file mode 100644 index 790ac52..0000000 --- a/lib/witchcraft/functor/functions.ex +++ /dev/null @@ -1,52 +0,0 @@ -defmodule Witchcraft.Functor.Functions do - alias Witchcraft.Functor, as: F - alias Witchcraft.Utility, as: U - - @doc ~S""" - Replace all of the input's data nodes with some constant value - - # Example - ``` - - iex> Witchcraft.Functor.Functions.map_replace([1,2,3], 42) - [42, 42, 42] - - ``` - """ - @spec map_replace(any, any) :: any - def map_replace(a, constant) do - F.lift(a, &(U.constant(&1, constant))) - end - - @doc ~S""" - Alias for `lift` with arguments flipped ('map over') - - # Example - - ``` - - iex> (&(&1 * 10)) <~ [1,2,3] - [10, 20, 30] - - ``` - - """ - @spec (any -> any) <~ any :: any - def func <~ args, do: F.lift(args, func) - - @doc ~S""" - Alias for `lift` - - # Example - - ``` - - iex> [1,2,3] ~> &(&1 * 10) - [10, 20, 30] - - ``` - - """ - @spec any ~> (any -> any) :: any - def args ~> func, do: func <~ args -end diff --git a/lib/witchcraft/functor/operator.ex b/lib/witchcraft/functor/operator.ex new file mode 100644 index 0000000..490a16f --- /dev/null +++ b/lib/witchcraft/functor/operator.ex @@ -0,0 +1,33 @@ +defmodule Witchcraft.Functor.Operator do + import Witchcraft.Functor, only: [lift: 2] + + @doc ~S""" + Alias for `lift`. As we'll see with `Witchcraft.Applicative`, + this arrow points in the direction of data flow (just like `|>`), but we often + prefer the function on the left side. + + ```elixir + + iex> (&(&1 * 10)) <~ [1,2,3] + [10, 20, 30] + + ``` + + """ + @spec (any -> any) <~ any :: any + def func <~ args, do: lift(args, func) + + @doc ~S""" + Alias for `lift` and `<~`, but with data flowing to the right. + + ```elixir + + iex> [1,2,3] ~> &(&1 * 10) + [10, 20, 30] + + ``` + + """ + @spec any ~> (any -> any) :: any + def args ~> func, do: func <~ args +end diff --git a/lib/witchcraft/functor/properties.ex b/lib/witchcraft/functor/property.ex similarity index 72% rename from lib/witchcraft/functor/properties.ex rename to lib/witchcraft/functor/property.ex index 98d076c..4c3403f 100644 --- a/lib/witchcraft/functor/properties.ex +++ b/lib/witchcraft/functor/property.ex @@ -1,21 +1,20 @@ -defmodule Witchcraft.Functor.Properties do - @moduledoc """ +defmodule Witchcraft.Functor.Property do + @moduledoc ~S""" Check samples of your functor to confirm that your data adheres to the functor properties. *All members* of your datatype should adhere to these rules. They are placed here as a quick way to spotcheck some of your values. """ - import Witchcraft.Utility - import Witchcraft.Functor - import Witchcraft.Functor.Functions + import Quark, only: [compose: 1, id: 1] + import Witchcraft.Functor, only: [lift: 2] @doc ~S""" Check that lifting a function into some context returns a member of the target type - ``` + ```elixir - iex> alias Witchcraft.Utility.Id, as: Id - iex> spotcheck_associates_object(%Id{id: 42}, &(&1), &Id.is_id&1) + iex> alias Witchcraft.Id, as: Id + iex> spotcheck_associates_object(%Id{id: 42}, &Quark.id/1, &Id.is_id/1) true ``` @@ -30,22 +29,19 @@ defmodule Witchcraft.Functor.Properties do Check that lifting a function does not interfere with identity. In other words, lifting `id(a)` shoud be the same as the identity of lifting `a`. - ``` + A ---- id ----> A - A ---- id ----> A + | | + (f) (f) + | | + v v - | | - (f) (f) - | | - v v + B ---- id ----> B - B ---- id ----> B - ``` - - ``` + ```elixir - iex> spotcheck_preserve_identity(%Witchcraft.Utility.Id{id: 7}, &(&1 + 1)) + iex> spotcheck_preserve_identity(%Witchcraft.Id{id: 7}, &(&1 + 1)) true ``` @@ -59,8 +55,13 @@ defmodule Witchcraft.Functor.Properties do @doc ~S""" Check that lifting a composed function is the same as lifting functions in sequence - iex> spotcheck_preserve_compositon(%Witchcraft.Utility.Id{id: 5}, &(&1 + 1), &(&1 * 10)) + ```elixir + + iex> spotcheck_preserve_compositon(%Witchcraft.Id{id: 5}, &(&1 + 1), &(&1 * 10)) true + + ``` + """ @spec spotcheck_preserve_compositon(any, (any -> any), (any -> any)) :: boolean def spotcheck_preserve_compositon(context, f, g) do @@ -70,9 +71,9 @@ defmodule Witchcraft.Functor.Properties do @doc ~S""" Spotcheck all functor properties - ``` + ```elixir - iex> alias Witchcraft.Utility.Id, as: Id + iex> alias Witchcraft.Id, as: Id iex> spotcheck(%Id{id: 42}, &(&1 + 1), &(&1 * 2), &Id.is_id&1) true diff --git a/lib/witchcraft/id.ex b/lib/witchcraft/id.ex new file mode 100644 index 0000000..e8d67c7 --- /dev/null +++ b/lib/witchcraft/id.ex @@ -0,0 +1,22 @@ +defmodule Witchcraft.Id do + @moduledoc ~S""" + A simple wrapper for some data. Only used in this library for examples. + + If you are interested in this type of functionality, please take a look at + [Algae](https://github.com/robot-overlord/algae). + """ + @type t :: %Witchcraft.Id{id: any} + defstruct [:id] + + def is_id(%Witchcraft.Id{id: _}), do: true + def is_id(_), do: false +end + +defmodule Witchcraft.Sad do + defstruct sad: -9.4 +end + +defimpl Witchcraft.Monoid, for: Witchcraft.Sad do + def identity(%Witchcraft.Sad{sad: _}), do: %Witchcraft.Sad{} + def append(%Witchcraft.Sad{sad: a}, %Witchcraft.Sad{sad: b}), do: %Witchcraft.Sad{sad: a / b} +end diff --git a/lib/witchcraft/monoid.ex b/lib/witchcraft/monoid.ex index 5c279ad..2f3469e 100644 --- a/lib/witchcraft/monoid.ex +++ b/lib/witchcraft/monoid.ex @@ -13,33 +13,15 @@ defprotocol Witchcraft.Monoid do - Unique element (`id`, sometimes called the 'zero' of the set) - Behaves as an identity with `op` - # Examples - ## Theory - ``` - # Pseudocode + identity = 0 op = &(&1 + &2) # Integer addition - op(34, identity) == 34 - ``` + append(34, identity) == 34 - ``` - # Pseudocode identity = 1 - op = &(&1 * &2) # Integer multiplication - op(42, identity) == 42 - ``` - - ## Concrete - ``` - - iex> alias Witchcraft.Monoid, as: Monoid - iex> defimpl Monoid, for: Integer do - iex> def identity(_), do: 0 - iex> def op(a, b), do: a + b - iex> end - iex> Monoid.op(1, 4) |> Monoid.op 2 |> Monoid.op 10 - 17 + append = &(&1 * &2) # Integer multiplication + append(42, identity) == 42 ``` @@ -57,6 +39,98 @@ defprotocol Witchcraft.Monoid do def identity(a) @doc "Combine two members of the monoid, and return another member" - @spec op(any, any) :: any - def op(a, b) + @spec append(any, any) :: any + def append(a, b) +end + +defimpl Witchcraft.Monoid, for: Integer do + @doc ~S""" + + ```elixir + + iex> identity(99) == identity(-9) + true + + ``` + + """ + def identity(_integer), do: 0 + + @doc ~S""" + + ```elixir + + iex> 1 |> append(4) |> append(2) |> append(10) + 17 + + ``` + + """ + @spec append(integer, integer) :: integer + def append(a, b), do: a + b +end + +defimpl Witchcraft.Monoid, for: Float do + @doc ~S""" + + ```elixir + + iex> identity(98.5) == identity(-8.5) + true + + ``` + + """ + def identity(_integer), do: 0.0 + + @doc ~S""" + + ```elixir + + iex> 1.0 |> append(4.0) |> append(2.0) |> append(10.1) + 17.1 + + ``` + + """ + def append(a, b), do: a + b +end + +defimpl Witchcraft.Monoid, for: BitString do + @doc ~S""" + + ```elixir + + iex> append(identity("welp"), "o hai") + "o hai" + + ``` + + """ + def identity(_), do: "" + + @doc ~S""" + + ```elixir + + iex> append("o hai", identity("welp")) + "o hai" + + iex> identity("") |> append(identity("")) == identity("") + true + + ``` + + """ + def append(a, b), do: a <> b +end + +defimpl Witchcraft.Monoid, for: List do + def identity(_list), do: [] + def append(as, bs), do: as ++ bs +end + +defimpl Witchcraft.Monoid, for: Map do + def identity(_map), do: %{} + def append(ma, mb), do: Dict.merge(ma, mb) end diff --git a/lib/witchcraft/monoid/functions.ex b/lib/witchcraft/monoid/functions.ex deleted file mode 100644 index c3827cf..0000000 --- a/lib/witchcraft/monoid/functions.ex +++ /dev/null @@ -1,24 +0,0 @@ -defmodule Witchcraft.Monoid.Functions do - alias Witchcraft.Monoid, as: Mon - - @doc ~S""" - Infix variant of `Monoid.op` - - # Example - ``` - - iex> alias Witchcraft.Monoid, as: Monoid - iex> defimpl Monoid, for: Integer do - iex> def identity(_), do: 0 - iex> def op(a, b), do: a + b - iex> end - iex> Monoid.op(1, 4) |> Monoid.op 2 |> Monoid.op 10 - 17 - iex> 1 <|> 4 <|> 2 <|> 10 - 17 - - ``` - """ - @spec any <|> any :: any - def a <|> b, do: Mon.op(a, b) -end diff --git a/lib/witchcraft/monoid/operator.ex b/lib/witchcraft/monoid/operator.ex new file mode 100644 index 0000000..4eef147 --- /dev/null +++ b/lib/witchcraft/monoid/operator.ex @@ -0,0 +1,34 @@ +defmodule Witchcraft.Monoid.Operator do + import Witchcraft.Monoid, only: [append: 2] + + @doc ~S""" + Infix variant of `Monoid.append` + + # Example + + ```elixir + + iex> import Witchcraft.Monoid + ...> defimpl Witchcraft.Monoid, for: Integer do + ...> def identity(_), do: 0 + ...> def append(a, b), do: a + b + ...> end + iex> 1 |> append(4) |> append(2) |> append(10) + 17 + + iex> 1 <|> 4 <|> 2 <|> 10 + 17 + + iex> import Witchcraft.Monoid + iex> 1 |> append(4) |> append(2) |> append(10) + 17 + + iex> [42, 43] <|> [44] <|> [45, 46] <|> [47] + [42, 43, 44, 45, 46, 47] + + ``` + + """ + @spec any <|> any :: any + def a <|> b, do: append(a, b) +end diff --git a/lib/witchcraft/monoid/properties.ex b/lib/witchcraft/monoid/property.ex similarity index 50% rename from lib/witchcraft/monoid/properties.ex rename to lib/witchcraft/monoid/property.ex index 1ff234a..29b7622 100644 --- a/lib/witchcraft/monoid/properties.ex +++ b/lib/witchcraft/monoid/property.ex @@ -1,4 +1,4 @@ -defmodule Witchcraft.Monoid.Properties do +defmodule Witchcraft.Monoid.Property do @moduledoc """ Check samples of your monoid to confirm that your data adheres to the monoidal properties. *All members* of your datatype should adhere to these rules. @@ -6,19 +6,41 @@ defmodule Witchcraft.Monoid.Properties do """ import Witchcraft.Monoid - import Witchcraft.Monoid.Functions + import Witchcraft.Monoid.Operator, only: [<|>: 2] - @doc """ + @doc ~S""" Check that some member of your monoid combines with the identity to return itself + + ```elixir + + iex> spotcheck_identity("well formed") + true + + # Float under division + iex> spotcheck_identity(%Witchcraft.Sad{}) + false + + ``` + """ @spec spotcheck_identity(any) :: boolean - def spotcheck_identity(member) do - (identity(member) <|> member) == member - end + def spotcheck_identity(member), do: (identity(member) <|> member) == member @doc ~S""" - Check that `Monoid.op` is [associative](https://en.wikipedia.org/wiki/Associative_property) + Check that `Monoid.append` is [associative](https://en.wikipedia.org/wiki/Associative_property) (ie: brackets don't matter) + + ```elixir + + iex> spotcheck_associativity("a", "b", "c") + true + + # Float under division + iex> spotcheck_associativity(%Witchcraft.Sad{sad: -9.1}, %Witchcraft.Sad{sad: 42.0}, %Witchcraft.Sad{sad: 88.8}) + false + + ``` + """ @spec spotcheck_associativity(any, any, any) :: boolean def spotcheck_associativity(member1, member2, member3) do @@ -27,6 +49,18 @@ defmodule Witchcraft.Monoid.Properties do @doc """ Spotcheck all monoid properties + + ```elixir + + iex> spotcheck(1,2,3) + true + + # Float under division + iex> spotcheck(%Witchcraft.Sad{sad: -9.1}, %Witchcraft.Sad{sad: 42.0}, %Witchcraft.Sad{sad: 88.8}) + false + + ``` + """ @spec spotcheck(any, any, any) :: boolean def spotcheck(a, b, c) do diff --git a/lib/witchcraft/utility.ex b/lib/witchcraft/utility.ex deleted file mode 100644 index acdacf0..0000000 --- a/lib/witchcraft/utility.ex +++ /dev/null @@ -1,98 +0,0 @@ -defmodule Witchcraft.Utility do - @doc ~S""" - Do nothing to an argument; just return it - - # Examples - ``` - - iex> Witchcraft.Utility.id("88 miles per hour") - "88 miles per hour" - - iex> Witchcraft.Utility.id(42) - 42 - - iex> Enum.map([1,2,3], &Witchcraft.Utility.id&1) - [1,2,3] - - ``` - """ - @spec id(any) :: any - def id(a), do: a - - @doc ~S""" - Return the *first* of two arguments. Can be used to repeatedly apply the same value - in functions such as folds. - - # Examples - ``` - - iex> Witchcraft.Utility.first(43, 42) - 43 - - iex> Enum.reduce([1,2,3], [42], &Witchcraft.Utility.first(&1, &2)) - 3 - - ``` - """ - @spec first(any, any) :: any - def first(a, _), do: a - - @doc ~S""" - Return the *second* of two arguments. Can be used to repeatedly apply the same value - in functions such as folds. - - # Examples - ``` - - iex> Witchcraft.Utility.second(43, 42) - 42 - - iex> Enum.reduce([1,2,3], [], &Witchcraft.Utility.second(&1, &2)) - [] - - ``` - """ - @spec second(any, any) :: any - def second(_, b), do: b - - @doc "Alias for `second/2`" - @spec constant(any, any) :: any - def constant(_, b), do: b - - @doc """ - Function composition, from the back of the lift to the front - - # Example - iex> sum_plus_one = Witchcraft.Utility.compose([&(&1 + 1), &(Enum.sum(&1))]) - iex> [1,2,3] |> sum_plus_one.() - 7 - """ - @spec compose([(... -> any)]) :: (... -> any) - def compose(func_list) do - List.foldr(func_list, &(id(&1)), fn(f, acc) -> &(f.(acc.(&1))) end) - end - - @doc ~S""" - Compose functions, from the head of the list of functions. The is the reverse - order versus what one would normally expect (left to right rather than right to left). - - # Example - iex> sum_plus_one = Witchcraft.Utility.reverse_compose([&(Enum.sum(&1)), &(&1 + 1)]) - iex> [1,2,3] |> sum_plus_one.() - 7 - """ - @spec reverse_compose([(... -> any)]) :: (... -> any) - def reverse_compose(func_list) do - Enum.reduce(func_list, &(id(&1)), fn(f, acc) -> &(f.(acc.(&1))) end) - end - - defmodule Id do - @moduledoc ~S""" - A simple container. Mainly used to show example implimentations of this library. - """ - defstruct id: nil - - def is_id(%Witchcraft.Utility.Id{id: _}), do: true - def is_id(_), do: false - end -end diff --git a/mix.exs b/mix.exs index 95e3fcb..2ddb29e 100644 --- a/mix.exs +++ b/mix.exs @@ -8,39 +8,29 @@ defmodule Witchcraft.Mixfile do description: "Common algebraic structures and functions", package: package, - version: "0.2.0", + version: "0.3.0", elixir: "~> 1.1", source_url: "https://github.com/robot-overlord/witchcraft", homepage_url: "https://github.com/robot-overlord/witchcraft", - build_embedded: Mix.env == :prod, - start_permanent: Mix.env == :prod, + # build_embedded: Mix.env == :prod, + # start_permanent: Mix.env == :prod, deps: deps, docs: [logo: "https://github.com/robot-overlord/witchcraft/blob/master/logo.png?raw=true", - extras: ["README.md"]]] + extras: ["README.md"]] + ] end - # Configuration for the OTP application - # - # Type "mix help compile.app" for more information - def application do - [applications: [:logger]] - end + # def application do + # [applications: [:logger]] + # end - # Dependencies can be Hex packages: - # - # {:mydep, "~> 0.3.0"} - # - # Or git/path repositories: - # - # {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"} - # - # Type "mix help deps" for more examples and options defp deps do [{:earmark, "~> 0.1", only: :dev}, - {:ex_doc, "~> 0.10", only: :dev}] + {:ex_doc, "~> 0.10", only: :dev}, + {:quark, "~> 1.0"}] end defp package do diff --git a/mix.lock b/mix.lock index 106cb04..6dcb5da 100644 --- a/mix.lock +++ b/mix.lock @@ -1,2 +1,4 @@ -%{"earmark": {:hex, :earmark, "0.1.17"}, - "ex_doc": {:hex, :ex_doc, "0.10.0"}} +%{"curry": {:hex, :curry, "0.0.1"}, + "earmark": {:hex, :earmark, "0.2.0"}, + "ex_doc": {:hex, :ex_doc, "0.11.3"}, + "quark": {:hex, :quark, "1.0.2"}} diff --git a/test/functor_test.exs b/test/functor_test.exs deleted file mode 100644 index b95a9f9..0000000 --- a/test/functor_test.exs +++ /dev/null @@ -1,11 +0,0 @@ -defmodule FunctorTest do - use ExUnit.Case - - import Witchcraft.Functor - import Witchcraft.Functor.Functions - import Witchcraft.Functor.Properties - - doctest Witchcraft.Functor - doctest Witchcraft.Functor.Functions - doctest Witchcraft.Functor.Properties -end diff --git a/test/monoid_test.exs b/test/monoid_test.exs deleted file mode 100644 index 2c9ca55..0000000 --- a/test/monoid_test.exs +++ /dev/null @@ -1,56 +0,0 @@ -defmodule MonoidTest do - use ExUnit.Case - - import Witchcraft.Monoid - import Witchcraft.Monoid.Functions - import Witchcraft.Monoid.Properties - - doctest Witchcraft.Monoid - doctest Witchcraft.Monoid.Functions - doctest Witchcraft.Monoid.Properties - - # Happy case: Strings - defimpl Witchcraft.Monoid, for: BitString do - def identity(_), do: "" - def op(a, b), do: a <> b - end - - # Sad case: Malformed floats under division - defimpl Witchcraft.Monoid, for: Float do - def identity(_), do: -9.0 - def op(a, b), do: a / b - end - - - test "identity always returns the same value for that datatype" do - assert identity("a") == identity("b") - end - - test "identity combined with itself is the identity" do - assert identity("") <|> identity("") == identity("") - end - - test "left identity" do - assert identity("welp") <|> "o hai" == "o hai" - end - - test "right identity" do - assert "o hai" <|> identity("welp") == "o hai" - end - - test "spotcheck_identity is true for well formed monoids" do - assert spotcheck_identity("well formed") == true - end - - test "spotcheck_identity is false for malformed monoids" do - assert spotcheck_identity(88.8) == false - end - - test "spotcheck_associativity returns true when monoid is well formed" do - assert spotcheck_associativity("a", "b", "c") == true - end - - test "spotcheck_associativity returns false when monoid is poorly formed" do - assert spotcheck_associativity(-9.1, 42.0, 88.8) == false - end -end diff --git a/test/utility_test.exs b/test/utility_test.exs deleted file mode 100644 index 363f58b..0000000 --- a/test/utility_test.exs +++ /dev/null @@ -1,5 +0,0 @@ -defmodule UtilityTest do - use ExUnit.Case - - doctest Witchcraft.Utility -end diff --git a/test/witchcraft_test.exs b/test/witchcraft_test.exs index 95e77fd..7554352 100644 --- a/test/witchcraft_test.exs +++ b/test/witchcraft_test.exs @@ -1,8 +1,38 @@ defmodule WitchcraftTest do use ExUnit.Case - doctest Witchcraft - test "the truth" do - assert 1 + 1 == 2 - end + # Monoid + # ====== + doctest Witchcraft.Monoid, import: true + + doctest Witchcraft.Monoid.Integer, import: true + doctest Witchcraft.Monoid.Float, import: true + doctest Witchcraft.Monoid.BitString, import: true + doctest Witchcraft.Monoid.List, import: true + doctest Witchcraft.Monoid.Map, import: true + + doctest Witchcraft.Monoid.Operator, import: true + doctest Witchcraft.Monoid.Property, import: true + + # Functor + # ======= + doctest Witchcraft.Functor, import: true + + doctest Witchcraft.Functor.List, import: true + doctest Witchcraft.Functor.Witchcraft.Id, import: true + + doctest Witchcraft.Functor.Function, import: true + doctest Witchcraft.Functor.Operator, import: true + doctest Witchcraft.Functor.Property, import: true + + # Applicative + # =========== + doctest Witchcraft.Applicative, import: true + + doctest Witchcraft.Applicative.List, import: true + doctest Witchcraft.Applicative.Witchcraft.Id, import: true + + doctest Witchcraft.Applicative.Function, import: true + doctest Witchcraft.Applicative.Operator, import: true + doctest Witchcraft.Applicative.Property, import: true end