From e13ec8fdeb4dec92c404b8cc3e06afd38fc5d3bc Mon Sep 17 00:00:00 2001 From: Brooklyn Zelenka Date: Thu, 12 Nov 2015 22:27:09 -0800 Subject: [PATCH 01/10] Applicative protocol --- README.md | 2 +- lib/witchcraft/applicative.ex | 72 +++++++++++++++++++ lib/witchcraft/functor.ex | 17 ++--- lib/witchcraft/functor/functions.ex | 22 ++---- lib/witchcraft/utility.ex | 106 +++++++++++++++++----------- mix.exs | 2 +- 6 files changed, 151 insertions(+), 70 deletions(-) create mode 100644 lib/witchcraft/applicative.ex 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/applicative.ex b/lib/witchcraft/applicative.ex new file mode 100644 index 0000000..692a149 --- /dev/null +++ b/lib/witchcraft/applicative.ex @@ -0,0 +1,72 @@ +defprotocol Witchcraft.Applicative do + require Witchcraft.Utility.Id + @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` + + iex> (v |> apply (wrap &(&1))) == v + True + + ## Composition + `apply` composes normally. + + iex> (wrap Witchcraft.Utility.<|>) <*> u <*> v <*> w == u <*> (v <*> w) + iex> (wrap compose) <*> u <*> v <*> w == u <*> (v <*> w) + True + + ## 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. + + iex> apply(wrap(x), wrap(f)) == wrap(f(x)) + True + + ## Interchange + The order does not matter when `apply`ing to a `wrap`ped value + and a `wrap`ped function. + + iex> u <*> pure y == pure ($ y) <*> u + iex> apply(wrap(y), u) == u |> apply wrap(&(lift(y, &1)) + True + + ## Functor + Being an applicative _functor_, `apply` behaves as `lift` on `wrap`ped values + + iex> lift f x == apply (wrap f) x + True + + """ + + @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, value) + + @doc ~S""" + Sequentially apply lifted function(s) to lifted data. + """ + @spec apply((any -> 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(_, value), do: value + + @doc ~S""" + For un`wrap`ped values, treat `apply` as plain function application. + """ + def apply(bare_value, bare_function), do: bare_function.(bare_value) +end diff --git a/lib/witchcraft/functor.ex b/lib/witchcraft/functor.ex index 833d880..8d24435 100644 --- a/lib/witchcraft/functor.ex +++ b/lib/witchcraft/functor.ex @@ -39,18 +39,15 @@ defprotocol Witchcraft.Functor do # Examples - ``` + iex> [1,2,3] |> Witchcraft.Functor.lift &(&1 + 1) + [2,3,4] - iex> Witchcraft.Functor.lift([1,2,3], &(&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.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} - - ``` """ @fallback_to_any true diff --git a/lib/witchcraft/functor/functions.ex b/lib/witchcraft/functor/functions.ex index 790ac52..860083d 100644 --- a/lib/witchcraft/functor/functions.ex +++ b/lib/witchcraft/functor/functions.ex @@ -6,12 +6,10 @@ defmodule Witchcraft.Functor.Functions do 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] + 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 @@ -23,12 +21,8 @@ defmodule Witchcraft.Functor.Functions do # Example - ``` - - iex> (&(&1 * 10)) <~ [1,2,3] - [10, 20, 30] - - ``` + iex> (&(&1 * 10)) <~ [1,2,3] + [10, 20, 30] """ @spec (any -> any) <~ any :: any @@ -39,12 +33,8 @@ defmodule Witchcraft.Functor.Functions do # Example - ``` - - iex> [1,2,3] ~> &(&1 * 10) - [10, 20, 30] - - ``` + iex> [1,2,3] ~> &(&1 * 10) + [10, 20, 30] """ @spec any ~> (any -> any) :: any diff --git a/lib/witchcraft/utility.ex b/lib/witchcraft/utility.ex index acdacf0..7ddabf2 100644 --- a/lib/witchcraft/utility.ex +++ b/lib/witchcraft/utility.ex @@ -2,19 +2,15 @@ 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("88 miles per hour") - "88 miles per hour" + iex> Witchcraft.Utility.id(42) + 42 - iex> Witchcraft.Utility.id(42) - 42 + iex> Enum.map([1,2,3], &Witchcraft.Utility.id&1) + [1,2,3] - iex> Enum.map([1,2,3], &Witchcraft.Utility.id&1) - [1,2,3] - - ``` """ @spec id(any) :: any def id(a), do: a @@ -23,16 +19,12 @@ defmodule Witchcraft.Utility do 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> Witchcraft.Utility.first(43, 42) + 43 - iex> Enum.reduce([1,2,3], [42], &Witchcraft.Utility.first(&1, &2)) - 3 + iex> Enum.reduce([1,2,3], [42], &Witchcraft.Utility.first(&1, &2)) + 3 - ``` """ @spec first(any, any) :: any def first(a, _), do: a @@ -41,16 +33,12 @@ defmodule Witchcraft.Utility do 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> Witchcraft.Utility.second(43, 42) + 42 - iex> Enum.reduce([1,2,3], [], &Witchcraft.Utility.second(&1, &2)) - [] + iex> Enum.reduce([1,2,3], [], &Witchcraft.Utility.second(&1, &2)) + [] - ``` """ @spec second(any, any) :: any def second(_, b), do: b @@ -62,29 +50,63 @@ defmodule Witchcraft.Utility do @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 + 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 + @spec compose((... -> any), (... -> any)) :: (... -> any) + def compose(g, f), do: &(g.(f.(&1))) + + @doc ~S""" + Infix compositon operator + + iex> sum_plus_one = &(&1 + 1) <|> &(Enum.sum(&1)) + iex> sum_plus_one.([1,2,3]) + 7 + + iex> pipe = [1,2,3] |> &(Enum.sum(&1)).() |> &(&1 + 1).() + iex> compose = [1,2,3] |> (&(&1 + 1) <|> &(Enum.sum(&1))).() + iex> pipe == compose + True + + """ + @spec (... -> any) <|> (... -> any) :: (... -> any) + def g <|> f, do: compose(g, f) + + @doc """ + Function composition, from the back of the lift to the front + + iex> sum_plus_one = Witchcraft.Utility.compose_forward(&(Enum.sum(&1)), &(&1 + 1)) + iex> [1,2,3] |> sum_plus_one.() + 7 + + """ + @spec compose_forward((... -> any), (... -> any)) :: (... -> any) + def compose_forward(f,g), do: &(g.(f.(&1))) + + @doc """ + Function composition, from the tail of the list to the head + + iex> sum_plus_one = Witchcraft.Utility.compose([&(&1 + 1), &(Enum.sum(&1))]) + iex> [1,2,3] |> sum_plus_one.() + 7 + + """ + @spec compose_list([(... -> any)]) :: (... -> any) + def compose_list(func_list), do: List.foldr(func_list, id, &compose(&1,&2)) @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 + iex> sum_plus_one = Witchcraft.Utility.compose_list_forward([&(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 + @spec compose_list_forward([(... -> any)]) :: (... -> any) + def reverse_compose_list(func_list), do: Enum.reduce(func_list, id, &compose(&1,&2)) defmodule Id do @moduledoc ~S""" diff --git a/mix.exs b/mix.exs index 95e3fcb..9c6af3b 100644 --- a/mix.exs +++ b/mix.exs @@ -8,7 +8,7 @@ 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", From e03bf8fc62dfafb84250bca42cf237d66acd5ff5 Mon Sep 17 00:00:00 2001 From: Brooklyn Zelenka Date: Sun, 27 Dec 2015 02:12:44 -0800 Subject: [PATCH 02/10] Elxir-ified the properties in the docs --- lib/witchcraft/applicative.ex | 26 +++++++--- lib/witchcraft/applicative/functions.ex | 63 ++++++++++++++++++++++++ lib/witchcraft/applicative/properties.ex | 57 +++++++++++++++++++++ lib/witchcraft/functor/functions.ex | 18 +++---- lib/witchcraft/functor/properties.ex | 49 +++++++----------- lib/witchcraft/utility.ex | 24 ++++++++- mix.lock | 3 +- 7 files changed, 186 insertions(+), 54 deletions(-) create mode 100644 lib/witchcraft/applicative/functions.ex create mode 100644 lib/witchcraft/applicative/properties.ex diff --git a/lib/witchcraft/applicative.ex b/lib/witchcraft/applicative.ex index 692a149..beafa46 100644 --- a/lib/witchcraft/applicative.ex +++ b/lib/witchcraft/applicative.ex @@ -10,37 +10,47 @@ defprotocol Witchcraft.Applicative do ## Identity `apply`ing a lifted `id` to some lifted value `v` does not change `v` - iex> (v |> apply (wrap &(&1))) == v + iex> apply(v, wrap(&id(&1))) == v True ## Composition `apply` composes normally. - iex> (wrap Witchcraft.Utility.<|>) <*> u <*> v <*> w == u <*> (v <*> w) - iex> (wrap compose) <*> u <*> v <*> w == u <*> (v <*> w) + iex> apply((wrap &compose(&1,&2)),(apply(u,(apply(v, w))))) == apply(u,(apply(v, w))) True ## 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. - iex> apply(wrap(x), wrap(f)) == wrap(f(x)) + iex> apply(wrap x, wrap f) == wrap f(x)) True ## Interchange The order does not matter when `apply`ing to a `wrap`ped value and a `wrap`ped function. - iex> u <*> pure y == pure ($ y) <*> u - iex> apply(wrap(y), u) == u |> apply wrap(&(lift(y, &1)) + iex> apply(wrap y, u) == apply(u, wrap &(lift(y, &1)) True ## Functor Being an applicative _functor_, `apply` behaves as `lift` on `wrap`ped values - iex> lift f x == apply (wrap f) x + iex> lift(x, f) == apply(x, (wrap f)) True + # Notes + Given that Elixir functons are right-associative, you can write clean looking, + but much more ambiguous versions: + + iex> wrap y |> apply u == u |> apply wrap &lift(y, &1) + True + + iex> x |> lift f == x |> apply wrap f + True + + However, it is strongly recommended to include the parentheses for clarity. + """ @fallback_to_any true @@ -55,7 +65,7 @@ defprotocol Witchcraft.Applicative do @doc ~S""" Sequentially apply lifted function(s) to lifted data. """ - @spec apply((any -> any), any) :: any + @spec apply(any, (... -> any)) :: any def apply(wrapped_value, wrapped_function) end diff --git a/lib/witchcraft/applicative/functions.ex b/lib/witchcraft/applicative/functions.ex new file mode 100644 index 0000000..4f05250 --- /dev/null +++ b/lib/witchcraft/applicative/functions.ex @@ -0,0 +1,63 @@ +defmodule Witchcraft.Applicative.Functions do + @moduledoc ~S""" + Function helpers, derivatives and operators for `Witchcraft.Applicative` + """ + + alias Witchcraft.Functor + alias Witchcraft.Applicative, as: A + alias Witchcraft.Utility, as: U + + @doc ~S""" + `lift` a function that takes a list of arguments + + 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], (... -> a)) :: any + def lift([value|[]], func), do: F.lift(value, func) + def lift([head|tail], func) do + lifted = U.curry(func) ~> head + Enum.reduce(tail, lifted, U.flip(apply)) + end + + @doc ~S""" + Infix alias for `Witchcraft.Applicative.apply` + + iex> [1,2,3] <<~ [&(&1 + 1), &(&1 * 10)] + [2,3,4,10,20,30] + + """ + @spec any <<~ (any -> any) :: any + def value <<~ func, do: apply(value, func) + + @doc ~S""" + Infix alias for `Witchcraft.Applicative.apply`, with arguments reversed. + This allows for easier reading in an classic "appicative style". + + iex> [&(&1 + 1), &(&1 * 10)] ~>> [1,2,3] + [2,3,4,10,20,30] + + """ + @spec (any -> any) ~>> any :: any + def func ~>> value, do: value <<~ func + + @doc ~S""" + Operator alias for `wrap`, punning on `@spec`'s `::`. + + iex> 1 ::: [] + [1] + + iex> "cool story, bro" ::: %U.Id{} + %U.Id{id: "cool story, bro"} + + """ + @spec any ::: any :: any + def bare ::: target, do: wrap(target, bare) +end diff --git a/lib/witchcraft/applicative/properties.ex b/lib/witchcraft/applicative/properties.ex new file mode 100644 index 0000000..31e1133 --- /dev/null +++ b/lib/witchcraft/applicative/properties.ex @@ -0,0 +1,57 @@ +defmoule Witchraft.Applicative.Properties do + @moduledoc ~S""" + """ + + import Witchcraft.Utility + import Witchcraft.Applicative + # import Witchcraft.Applicative.Functions + + @doc ~S""" + `apply`ing a lifted `id` to some lifted value `v` does not change `v` + """ + @spec spotcheck_applicative_identity() + def spotcheck_applicative_identity(value) do + value ~>> wrap &id(&1) == value + end + + ## Composition + `apply` composes normally. + + iex> apply((wrap &compose(&1,&2)),(apply(u,(apply(v, w))))) == apply(u,(apply(v, w))) + True + + ## 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. + + iex> apply(wrap x, wrap f) == wrap f(x)) + True + + ## Interchange + The order does not matter when `apply`ing to a `wrap`ped value + and a `wrap`ped function. + + iex> apply(wrap y, u) == apply(u, wrap &(lift(y, &1)) + True + + ## Functor + Being an applicative _functor_, `apply` behaves as `lift` on `wrap`ped values + + iex> lift(x, f) == apply(x, (wrap f)) + True + + # Notes + Given that Elixir functons are right-associative, you can write clean looking, + but much more ambiguous versions: + + iex> wrap y |> apply u == u |> apply wrap &lift(y, &1) + True + + iex> x |> lift f == x |> apply wrap f + True + + However, it is strongly recommended to include the parentheses for clarity. + """ + + +end diff --git a/lib/witchcraft/functor/functions.ex b/lib/witchcraft/functor/functions.ex index 860083d..31549e3 100644 --- a/lib/witchcraft/functor/functions.ex +++ b/lib/witchcraft/functor/functions.ex @@ -5,8 +5,6 @@ defmodule Witchcraft.Functor.Functions do @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] @@ -19,24 +17,20 @@ defmodule Witchcraft.Functor.Functions do @doc ~S""" Alias for `lift` with arguments flipped ('map over') - # Example - - iex> (&(&1 * 10)) <~ [1,2,3] + iex> (&(&1 * 10)) ~> [1,2,3] [10, 20, 30] """ - @spec (any -> any) <~ any :: any - def func <~ args, do: F.lift(args, func) + @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) + iex> [1,2,3] <~ &(&1 * 10) [10, 20, 30] """ - @spec any ~> (any -> any) :: any - def args ~> func, do: func <~ args + @spec any <~ (any -> any) :: any + def args <~ func, do: func ~> args end diff --git a/lib/witchcraft/functor/properties.ex b/lib/witchcraft/functor/properties.ex index 98d076c..79deae8 100644 --- a/lib/witchcraft/functor/properties.ex +++ b/lib/witchcraft/functor/properties.ex @@ -1,5 +1,5 @@ defmodule Witchcraft.Functor.Properties do - @moduledoc """ + @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. @@ -12,13 +12,9 @@ defmodule Witchcraft.Functor.Properties do @doc ~S""" Check that lifting a function into some context returns a member of the target type - ``` - - iex> alias Witchcraft.Utility.Id, as: Id - iex> spotcheck_associates_object(%Id{id: 42}, &(&1), &Id.is_id&1) - true - - ``` + iex> alias Witchcraft.Utility.Id, as: Id + iex> spotcheck_associates_object(%Id{id: 42}, &(&1), &Id.is_id&1) + true """ @spec spotcheck_associates_object(any, (any -> any), (any -> boolean)) :: boolean @@ -30,25 +26,18 @@ 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 - - | | - (f) (f) - | | - v v + A ---- id ----> A - B ---- id ----> B + | | + (f) (f) + | | + v v - ``` + B ---- id ----> B - ``` - iex> spotcheck_preserve_identity(%Witchcraft.Utility.Id{id: 7}, &(&1 + 1)) - true - - ``` + iex> spotcheck_preserve_identity(%Witchcraft.Utility.Id{id: 7}, &(&1 + 1)) + true """ @spec spotcheck_preserve_identity(any, (any -> any)) :: boolean @@ -59,8 +48,8 @@ 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)) - true + iex> spotcheck_preserve_compositon(%Witchcraft.Utility.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,13 +59,9 @@ defmodule Witchcraft.Functor.Properties do @doc ~S""" Spotcheck all functor properties - ``` - - iex> alias Witchcraft.Utility.Id, as: Id - iex> spotcheck(%Id{id: 42}, &(&1 + 1), &(&1 * 2), &Id.is_id&1) - true - - ``` + iex> alias Witchcraft.Utility.Id, as: Id + iex> spotcheck(%Id{id: 42}, &(&1 + 1), &(&1 * 2), &Id.is_id&1) + true """ @spec spotcheck(any, (any -> any), (any -> any), (any -> boolean)) :: boolean diff --git a/lib/witchcraft/utility.ex b/lib/witchcraft/utility.ex index 7ddabf2..31a824b 100644 --- a/lib/witchcraft/utility.ex +++ b/lib/witchcraft/utility.ex @@ -47,7 +47,13 @@ defmodule Witchcraft.Utility do @spec constant(any, any) :: any def constant(_, b), do: b - @doc """ + @doc ~S""" + Reverse the order fo two arguments + """ + @spec flip(((any, any) -> any)) :: (any, any) -> any + def flip(func), do: &func.(&2, &1) + + @doc ~S""" Function composition, from the back of the lift to the front iex> sum_plus_one = Witchcraft.Utility.compose(&(&1 + 1), &(Enum.sum(&1))) @@ -108,6 +114,22 @@ defmodule Witchcraft.Utility do @spec compose_list_forward([(... -> any)]) :: (... -> any) def reverse_compose_list(func_list), do: Enum.reduce(func_list, id, &compose(&1,&2)) + @doc ~S""" + Curry is copied from http://blog.patrikstorm.com/function-currying-in-elixir + """ + def curry(fun) do + {_, arity} = :erlang.fun_info(fun, :arity) + curry(fun, arity) + end + + def curry(fun, 0, arguments) do + apply(fun, Enum.reverse arguments) + end + + def curry(fun, arity, arguments) do + fn arg -> curry(fun, arity - 1, [arg | arguments]) end + end + defmodule Id do @moduledoc ~S""" A simple container. Mainly used to show example implimentations of this library. diff --git a/mix.lock b/mix.lock index 106cb04..1305ad8 100644 --- a/mix.lock +++ b/mix.lock @@ -1,2 +1,3 @@ -%{"earmark": {:hex, :earmark, "0.1.17"}, +%{"curry": {:hex, :curry, "0.0.1"}, + "earmark": {:hex, :earmark, "0.1.17"}, "ex_doc": {:hex, :ex_doc, "0.10.0"}} From 189313342be1c4ec55dd51071cdf4486b68e8a28 Mon Sep 17 00:00:00 2001 From: Brooklyn Zelenka Date: Sat, 2 Jan 2016 17:03:43 -0800 Subject: [PATCH 03/10] Substitute Quark for Witchcraft.Utility --- lib/witchcraft.ex | 25 ----- lib/witchcraft/applicative.ex | 4 +- lib/witchcraft/applicative/functions.ex | 13 --- lib/witchcraft/utility.ex | 142 ------------------------ mix.exs | 3 +- mix.lock | 3 +- 6 files changed, 6 insertions(+), 184 deletions(-) delete mode 100644 lib/witchcraft.ex delete mode 100644 lib/witchcraft/utility.ex 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/applicative.ex b/lib/witchcraft/applicative.ex index beafa46..91803a5 100644 --- a/lib/witchcraft/applicative.ex +++ b/lib/witchcraft/applicative.ex @@ -60,7 +60,7 @@ defprotocol Witchcraft.Applicative do or empty value of that type, but not nessesarily). """ @spec wrap(any, any) :: any - def wrap(specimen, value) + def wrap(specimen, bare) @doc ~S""" Sequentially apply lifted function(s) to lifted data. @@ -73,7 +73,7 @@ defimpl Witchcraft.Applicative, for: Any do @doc ~S""" By default, use the true identity functor (ie: don't wrap) """ - def wrap(_, value), do: value + def wrap(_, bare), do: bare @doc ~S""" For un`wrap`ped values, treat `apply` as plain function application. diff --git a/lib/witchcraft/applicative/functions.ex b/lib/witchcraft/applicative/functions.ex index 4f05250..d4e12fe 100644 --- a/lib/witchcraft/applicative/functions.ex +++ b/lib/witchcraft/applicative/functions.ex @@ -47,17 +47,4 @@ defmodule Witchcraft.Applicative.Functions do """ @spec (any -> any) ~>> any :: any def func ~>> value, do: value <<~ func - - @doc ~S""" - Operator alias for `wrap`, punning on `@spec`'s `::`. - - iex> 1 ::: [] - [1] - - iex> "cool story, bro" ::: %U.Id{} - %U.Id{id: "cool story, bro"} - - """ - @spec any ::: any :: any - def bare ::: target, do: wrap(target, bare) end diff --git a/lib/witchcraft/utility.ex b/lib/witchcraft/utility.ex deleted file mode 100644 index 31a824b..0000000 --- a/lib/witchcraft/utility.ex +++ /dev/null @@ -1,142 +0,0 @@ -defmodule Witchcraft.Utility do - @doc ~S""" - Do nothing to an argument; just return it - - 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. - - 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. - - 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 ~S""" - Reverse the order fo two arguments - """ - @spec flip(((any, any) -> any)) :: (any, any) -> any - def flip(func), do: &func.(&2, &1) - - @doc ~S""" - Function composition, from the back of the lift to the front - - iex> sum_plus_one = Witchcraft.Utility.compose(&(&1 + 1), &(Enum.sum(&1))) - iex> [1,2,3] |> sum_plus_one.() - 7 - - """ - @spec compose((... -> any), (... -> any)) :: (... -> any) - def compose(g, f), do: &(g.(f.(&1))) - - @doc ~S""" - Infix compositon operator - - iex> sum_plus_one = &(&1 + 1) <|> &(Enum.sum(&1)) - iex> sum_plus_one.([1,2,3]) - 7 - - iex> pipe = [1,2,3] |> &(Enum.sum(&1)).() |> &(&1 + 1).() - iex> compose = [1,2,3] |> (&(&1 + 1) <|> &(Enum.sum(&1))).() - iex> pipe == compose - True - - """ - @spec (... -> any) <|> (... -> any) :: (... -> any) - def g <|> f, do: compose(g, f) - - @doc """ - Function composition, from the back of the lift to the front - - iex> sum_plus_one = Witchcraft.Utility.compose_forward(&(Enum.sum(&1)), &(&1 + 1)) - iex> [1,2,3] |> sum_plus_one.() - 7 - - """ - @spec compose_forward((... -> any), (... -> any)) :: (... -> any) - def compose_forward(f,g), do: &(g.(f.(&1))) - - @doc """ - Function composition, from the tail of the list to the head - - iex> sum_plus_one = Witchcraft.Utility.compose([&(&1 + 1), &(Enum.sum(&1))]) - iex> [1,2,3] |> sum_plus_one.() - 7 - - """ - @spec compose_list([(... -> any)]) :: (... -> any) - def compose_list(func_list), do: List.foldr(func_list, id, &compose(&1,&2)) - - @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). - - iex> sum_plus_one = Witchcraft.Utility.compose_list_forward([&(Enum.sum(&1)), &(&1 + 1)]) - iex> [1,2,3] |> sum_plus_one.() - 7 - - """ - @spec compose_list_forward([(... -> any)]) :: (... -> any) - def reverse_compose_list(func_list), do: Enum.reduce(func_list, id, &compose(&1,&2)) - - @doc ~S""" - Curry is copied from http://blog.patrikstorm.com/function-currying-in-elixir - """ - def curry(fun) do - {_, arity} = :erlang.fun_info(fun, :arity) - curry(fun, arity) - end - - def curry(fun, 0, arguments) do - apply(fun, Enum.reverse arguments) - end - - def curry(fun, arity, arguments) do - fn arg -> curry(fun, arity - 1, [arg | arguments]) 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 9c6af3b..683bd46 100644 --- a/mix.exs +++ b/mix.exs @@ -40,7 +40,8 @@ defmodule Witchcraft.Mixfile do # 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", only: :dev}] end defp package do diff --git a/mix.lock b/mix.lock index 1305ad8..49120e7 100644 --- a/mix.lock +++ b/mix.lock @@ -1,3 +1,4 @@ %{"curry": {:hex, :curry, "0.0.1"}, "earmark": {:hex, :earmark, "0.1.17"}, - "ex_doc": {:hex, :ex_doc, "0.10.0"}} + "ex_doc": {:hex, :ex_doc, "0.10.0"}, + "quark": {:hex, :quark, "1.0.0"}} From 999308aa1b62bb3276e859f7e94873cb15093287 Mon Sep 17 00:00:00 2001 From: Brooklyn Zelenka Date: Sat, 2 Jan 2016 17:13:30 -0800 Subject: [PATCH 04/10] Witchcraft.Utility.Id -> Witchcraft.ADT.Id --- lib/witchcraft/applicative.ex | 2 +- lib/witchcraft/applicative/functions.ex | 6 +++--- lib/witchcraft/applicative/properties.ex | 3 ++- lib/witchcraft/functor.ex | 14 +++++++------- lib/witchcraft/functor/functions.ex | 10 ++++++++-- lib/witchcraft/functor/properties.ex | 12 ++++++------ test/utility_test.exs | 5 ----- 7 files changed, 27 insertions(+), 25 deletions(-) delete mode 100644 test/utility_test.exs diff --git a/lib/witchcraft/applicative.ex b/lib/witchcraft/applicative.ex index 91803a5..aa84dd3 100644 --- a/lib/witchcraft/applicative.ex +++ b/lib/witchcraft/applicative.ex @@ -1,5 +1,5 @@ defprotocol Witchcraft.Applicative do - require Witchcraft.Utility.Id + require Witchcraft.ADT.Id @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 diff --git a/lib/witchcraft/applicative/functions.ex b/lib/witchcraft/applicative/functions.ex index d4e12fe..10f9a28 100644 --- a/lib/witchcraft/applicative/functions.ex +++ b/lib/witchcraft/applicative/functions.ex @@ -5,7 +5,7 @@ defmodule Witchcraft.Applicative.Functions do alias Witchcraft.Functor alias Witchcraft.Applicative, as: A - alias Witchcraft.Utility, as: U + alias Quark, as: Q @doc ~S""" `lift` a function that takes a list of arguments @@ -23,8 +23,8 @@ defmodule Witchcraft.Applicative.Functions do @spec lift([any], (... -> a)) :: any def lift([value|[]], func), do: F.lift(value, func) def lift([head|tail], func) do - lifted = U.curry(func) ~> head - Enum.reduce(tail, lifted, U.flip(apply)) + lifted = Q.Curry.curry(func) ~> head + Enum.reduce(tail, lifted, Q.flip(apply)) end @doc ~S""" diff --git a/lib/witchcraft/applicative/properties.ex b/lib/witchcraft/applicative/properties.ex index 31e1133..d420dda 100644 --- a/lib/witchcraft/applicative/properties.ex +++ b/lib/witchcraft/applicative/properties.ex @@ -2,10 +2,11 @@ defmoule Witchraft.Applicative.Properties do @moduledoc ~S""" """ - import Witchcraft.Utility import Witchcraft.Applicative # import Witchcraft.Applicative.Functions + import Quark + @doc ~S""" `apply`ing a lifted `id` to some lifted value `v` does not change `v` """ diff --git a/lib/witchcraft/functor.ex b/lib/witchcraft/functor.ex index 8d24435..5abfe16 100644 --- a/lib/witchcraft/functor.ex +++ b/lib/witchcraft/functor.ex @@ -1,5 +1,5 @@ defprotocol Witchcraft.Functor do - require Witchcraft.Utility.Id + require Witchcraft.ADT.Id @moduledoc ~S""" Functors provide a way to apply a function to value(s) a datatype (lists, trees, maybes, etc). @@ -42,11 +42,11 @@ defprotocol Witchcraft.Functor do iex> [1,2,3] |> Witchcraft.Functor.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> defimpl Witchcraft.Functor, for: Witchcraft.ADT.Id do + iex> def lift(%Witchcraft.ADT.Id{id: inner}, func), do: %Witchcraft.ADT.Id{id: func.(inner)} iex> end - iex> Witchcraft.Functor.lift(%Witchcraft.Utility.Id{id: 1}, &(&1 + 1)) - %Witchcraft.Utility.Id{id: 2} + iex> Witchcraft.Functor.lift(%Witchcraft.ADT.Id{id: 1}, &(&1 + 1)) + %Witchcraft.ADT.Id{id: 2} """ @@ -65,7 +65,7 @@ defimpl Witchcraft.Functor, for: Any do def lift(data, func), do: Enum.map(data, func) end -defimpl Witchcraft.Functor, for: Witchcraft.Utility.Id do +defimpl Witchcraft.Functor, for: Witchcraft.ADT.Id do @doc "Example struct implimentation" - def lift(%Witchcraft.Utility.Id{id: data}, func), do: %Witchcraft.Utility.Id{id: func.(data)} + def lift(%Witchcraft.ADT.Id{id: data}, func), do: %Witchcraft.ADT.Id{id: func.(data)} end diff --git a/lib/witchcraft/functor/functions.ex b/lib/witchcraft/functor/functions.ex index 31549e3..9beae3a 100644 --- a/lib/witchcraft/functor/functions.ex +++ b/lib/witchcraft/functor/functions.ex @@ -1,6 +1,12 @@ defmodule Witchcraft.Functor.Functions do + @moduledoc ~S""" + """ + + alias Quark, as: Q alias Witchcraft.Functor, as: F - alias Witchcraft.Utility, as: U + + @spec lift((... -> any)) :: (any -> any) + def lift(fun), do: &lift(&1, fun) @doc ~S""" Replace all of the input's data nodes with some constant value @@ -11,7 +17,7 @@ defmodule Witchcraft.Functor.Functions do """ @spec map_replace(any, any) :: any def map_replace(a, constant) do - F.lift(a, &(U.constant(&1, constant))) + F.lift(a, &(Q.const(&1, constant))) end @doc ~S""" diff --git a/lib/witchcraft/functor/properties.ex b/lib/witchcraft/functor/properties.ex index 79deae8..907b656 100644 --- a/lib/witchcraft/functor/properties.ex +++ b/lib/witchcraft/functor/properties.ex @@ -5,15 +5,15 @@ defmodule Witchcraft.Functor.Properties do They are placed here as a quick way to spotcheck some of your values. """ - import Witchcraft.Utility + import Quark, only: [id: 1] import Witchcraft.Functor import Witchcraft.Functor.Functions @doc ~S""" Check that lifting a function into some context returns a member of the target type - iex> alias Witchcraft.Utility.Id, as: Id - iex> spotcheck_associates_object(%Id{id: 42}, &(&1), &Id.is_id&1) + iex> alias Witchcraft.ADT.Id, as: Id + iex> spotcheck_associates_object(%Id{id: 42}, &(&1), &Id.is_id/1) true """ @@ -36,7 +36,7 @@ defmodule Witchcraft.Functor.Properties do B ---- id ----> B - iex> spotcheck_preserve_identity(%Witchcraft.Utility.Id{id: 7}, &(&1 + 1)) + iex> spotcheck_preserve_identity(%Witchcraft.ADT.Id{id: 7}, &(&1 + 1)) true """ @@ -48,7 +48,7 @@ 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)) + iex> spotcheck_preserve_compositon(%Witchcraft.ADT.Id{id: 5}, &(&1 + 1), &(&1 * 10)) true """ @spec spotcheck_preserve_compositon(any, (any -> any), (any -> any)) :: boolean @@ -59,7 +59,7 @@ defmodule Witchcraft.Functor.Properties do @doc ~S""" Spotcheck all functor properties - iex> alias Witchcraft.Utility.Id, as: Id + iex> alias Witchcraft.ADT.Id, as: Id iex> spotcheck(%Id{id: 42}, &(&1 + 1), &(&1 * 2), &Id.is_id&1) true 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 From 1ae47250bcd367e6cc58ff877b9e5bbd50529e25 Mon Sep 17 00:00:00 2001 From: Brooklyn Zelenka Date: Fri, 8 Jan 2016 15:55:15 -0800 Subject: [PATCH 05/10] Monoid.op -> Monoid.append --- lib/witchcraft/monoid.ex | 54 ++++++++++++++++++++++++++++------------ test/witchcraft_test.exs | 5 +--- 2 files changed, 39 insertions(+), 20 deletions(-) diff --git a/lib/witchcraft/monoid.ex b/lib/witchcraft/monoid.ex index 5c279ad..1c65a0b 100644 --- a/lib/witchcraft/monoid.ex +++ b/lib/witchcraft/monoid.ex @@ -16,29 +16,29 @@ defprotocol Witchcraft.Monoid do # Examples ## Theory - ``` + ```elixir + # 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 + append = &(&1 * &2) # Integer multiplication + append(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 + ```elixir + + iex> defimpl Witchcraft.Monoid, for: Integer do + ...> def identity(_), do: 0 + ...> def append(a, b), do: a + b + ...> end + iex> + iex> 1 |> op 4 |> op 2 |> op 10 17 ``` @@ -52,11 +52,33 @@ defprotocol Witchcraft.Monoid do For the protocol to operate as intended, you need to respect the above properties. """ + @fallback_to_any true + @doc "Get the identity ('zero') element of the monoid by passing in any element of the set" @spec identity(any) :: any 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 + def identity(_integer), do: 0 + def append(a, b), do: a + b +end + +defimpl Witchcraft.Monoid, for: Float do + def identity(_integer), do: 0.0 + 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/test/witchcraft_test.exs b/test/witchcraft_test.exs index 95e77fd..8ce47c0 100644 --- a/test/witchcraft_test.exs +++ b/test/witchcraft_test.exs @@ -1,8 +1,5 @@ defmodule WitchcraftTest do use ExUnit.Case - doctest Witchcraft - test "the truth" do - assert 1 + 1 == 2 - end + doctest Witchcraft.Monoid, import: true end From b914c9219ceea80dcf57279f60c81babc9315c47 Mon Sep 17 00:00:00 2001 From: Brooklyn Zelenka Date: Fri, 8 Jan 2016 16:14:30 -0800 Subject: [PATCH 06/10] Factor out ADT, as is now Algae --- lib/witchcraft/adt/maybe.ex | 48 ------------- lib/witchcraft/adt/maybe_plus.ex | 50 -------------- lib/witchcraft/applicative.ex | 7 +- lib/witchcraft/applicative/function.ex | 28 ++++++++ lib/witchcraft/applicative/functions.ex | 50 -------------- lib/witchcraft/applicative/operator.ex | 22 ++++++ .../{properties.ex => property.ex} | 69 +++++++++++-------- lib/witchcraft/functor.ex | 19 +++-- lib/witchcraft/functor/function.ex | 19 +++++ lib/witchcraft/functor/functions.ex | 42 ----------- lib/witchcraft/functor/operator.ex | 21 ++++++ .../functor/{properties.ex => property.ex} | 4 +- lib/witchcraft/id.ex | 10 +++ lib/witchcraft/monoid.ex | 2 - lib/witchcraft/monoid/functions.ex | 24 ------- lib/witchcraft/monoid/operator.ex | 42 +++++++++++ .../monoid/{properties.ex => property.ex} | 10 +-- test/functor_test.exs | 18 ++--- test/witchcraft_test.exs | 2 + 19 files changed, 214 insertions(+), 273 deletions(-) delete mode 100644 lib/witchcraft/adt/maybe.ex delete mode 100644 lib/witchcraft/adt/maybe_plus.ex create mode 100644 lib/witchcraft/applicative/function.ex delete mode 100644 lib/witchcraft/applicative/functions.ex create mode 100644 lib/witchcraft/applicative/operator.ex rename lib/witchcraft/applicative/{properties.ex => property.ex} (55%) create mode 100644 lib/witchcraft/functor/function.ex delete mode 100644 lib/witchcraft/functor/functions.ex create mode 100644 lib/witchcraft/functor/operator.ex rename lib/witchcraft/functor/{properties.ex => property.ex} (96%) create mode 100644 lib/witchcraft/id.ex delete mode 100644 lib/witchcraft/monoid/functions.ex create mode 100644 lib/witchcraft/monoid/operator.ex rename lib/witchcraft/monoid/{properties.ex => property.ex} (73%) 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 index aa84dd3..df988a6 100644 --- a/lib/witchcraft/applicative.ex +++ b/lib/witchcraft/applicative.ex @@ -1,5 +1,4 @@ defprotocol Witchcraft.Applicative do - require Witchcraft.ADT.Id @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 @@ -53,6 +52,8 @@ defprotocol Witchcraft.Applicative do """ + require Witchcraft.Id + @fallback_to_any true @doc ~S""" @@ -73,10 +74,10 @@ defimpl Witchcraft.Applicative, for: Any do @doc ~S""" By default, use the true identity functor (ie: don't wrap) """ - def wrap(_, bare), do: bare + 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: bare_function.(bare_value) + def apply(bare_value, bare_function), do: Quary.curry(bare_function).(bare_value) end diff --git a/lib/witchcraft/applicative/function.ex b/lib/witchcraft/applicative/function.ex new file mode 100644 index 0000000..6aa82e0 --- /dev/null +++ b/lib/witchcraft/applicative/function.ex @@ -0,0 +1,28 @@ +defmodule Witchcraft.Applicative.Function do + @moduledoc ~S""" + Function helpers, derivatives and operators for `Witchcraft.Applicative` + """ + + import Witchcraft.Functor.Operator + + @doc ~S""" + `lift` a function that takes a list of arguments + + 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], (... -> a)) :: any + def lift([value|[]], func), do: value <~ func + + def lift([head|tail], func) do + lifted = Quark.Curry.curry(func) ~> head + Enum.reduce(tail, lifted, Quark.flip(apply)) + end +end diff --git a/lib/witchcraft/applicative/functions.ex b/lib/witchcraft/applicative/functions.ex deleted file mode 100644 index 10f9a28..0000000 --- a/lib/witchcraft/applicative/functions.ex +++ /dev/null @@ -1,50 +0,0 @@ -defmodule Witchcraft.Applicative.Functions do - @moduledoc ~S""" - Function helpers, derivatives and operators for `Witchcraft.Applicative` - """ - - alias Witchcraft.Functor - alias Witchcraft.Applicative, as: A - alias Quark, as: Q - - @doc ~S""" - `lift` a function that takes a list of arguments - - 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], (... -> a)) :: any - def lift([value|[]], func), do: F.lift(value, func) - def lift([head|tail], func) do - lifted = Q.Curry.curry(func) ~> head - Enum.reduce(tail, lifted, Q.flip(apply)) - end - - @doc ~S""" - Infix alias for `Witchcraft.Applicative.apply` - - iex> [1,2,3] <<~ [&(&1 + 1), &(&1 * 10)] - [2,3,4,10,20,30] - - """ - @spec any <<~ (any -> any) :: any - def value <<~ func, do: apply(value, func) - - @doc ~S""" - Infix alias for `Witchcraft.Applicative.apply`, with arguments reversed. - This allows for easier reading in an classic "appicative style". - - iex> [&(&1 + 1), &(&1 * 10)] ~>> [1,2,3] - [2,3,4,10,20,30] - - """ - @spec (any -> any) ~>> any :: any - def func ~>> value, do: value <<~ func -end diff --git a/lib/witchcraft/applicative/operator.ex b/lib/witchcraft/applicative/operator.ex new file mode 100644 index 0000000..5505b20 --- /dev/null +++ b/lib/witchcraft/applicative/operator.ex @@ -0,0 +1,22 @@ +defmodule Witchcraft.Applicative.Operator do + @doc ~S""" + Infix alias for `Witchcraft.Applicative.apply` + + iex> [1,2,3] <<~ [&(&1 + 1), &(&1 * 10)] + [2,3,4,10,20,30] + + """ + @spec any <<~ (any -> any) :: any + def value <<~ func, do: Witchcraft.Applicative.apply(value, func) + + @doc ~S""" + Infix alias for `Witchcraft.Applicative.apply`, with arguments reversed. + This allows for easier reading in an classic "appicative style". + + iex> [&(&1 + 1), &(&1 * 10)] ~>> [1,2,3] + [2,3,4,10,20,30] + + """ + @spec (any -> any) ~>> any :: any + def func ~>> value, do: value <<~ func +end diff --git a/lib/witchcraft/applicative/properties.ex b/lib/witchcraft/applicative/property.ex similarity index 55% rename from lib/witchcraft/applicative/properties.ex rename to lib/witchcraft/applicative/property.ex index d420dda..4fe6194 100644 --- a/lib/witchcraft/applicative/properties.ex +++ b/lib/witchcraft/applicative/property.ex @@ -1,58 +1,71 @@ -defmoule Witchraft.Applicative.Properties do +defmoule Witchraft.Applicative.Property do @moduledoc ~S""" - """ - - import Witchcraft.Applicative - # import Witchcraft.Applicative.Functions - - import Quark - - @doc ~S""" - `apply`ing a lifted `id` to some lifted value `v` does not change `v` - """ - @spec spotcheck_applicative_identity() - def spotcheck_applicative_identity(value) do - value ~>> wrap &id(&1) == value - end - ## Composition `apply` composes normally. - iex> apply((wrap &compose(&1,&2)),(apply(u,(apply(v, w))))) == apply(u,(apply(v, w))) - True + ```elixir + + iex> apply((wrap &compose(&1,&2)),(apply(u,(apply(v, w))))) == apply(u,(apply(v, w))) + True + + ``` ## 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. - iex> apply(wrap x, wrap f) == wrap f(x)) - True + ```elixir + + iex> apply(wrap x, wrap f) == wrap f(x) + True + + ``` ## Interchange The order does not matter when `apply`ing to a `wrap`ped value and a `wrap`ped function. - iex> apply(wrap y, u) == apply(u, wrap &(lift(y, &1)) - True + ```elixir + + iex> apply(wrap y, u) == apply(u, wrap &(lift(y, &1)) + True + + ``` ## Functor Being an applicative _functor_, `apply` behaves as `lift` on `wrap`ped values - iex> lift(x, f) == apply(x, (wrap f)) - True + ```elixir + + iex> lift(x, f) == apply(x, (wrap f)) + True + + ``` # Notes Given that Elixir functons are right-associative, you can write clean looking, but much more ambiguous versions: - iex> wrap y |> apply u == u |> apply wrap &lift(y, &1) - True + ```elixir + + iex> wrap y |> apply u == u |> apply wrap &lift(y, &1) + True - iex> x |> lift f == x |> apply wrap f - True + iex> x |> lift f == x |> apply wrap f + True + + ``` However, it is strongly recommended to include the parentheses for clarity. """ + import Quark, only: [id: 1] + import Witchcraft.Applicative + import Witchcraft.Applicative.Function + @doc ~S""" + `apply`ing a lifted `id` to some lifted value `v` does not change `v` + """ + @spec spotcheck_applicative_identity() + def spotcheck_applicative_identity(value, do: value ~>> wrap id == value end diff --git a/lib/witchcraft/functor.ex b/lib/witchcraft/functor.ex index 5abfe16..4062825 100644 --- a/lib/witchcraft/functor.ex +++ b/lib/witchcraft/functor.ex @@ -1,5 +1,6 @@ defprotocol Witchcraft.Functor do - require Witchcraft.ADT.Id + require Witchcraft.Id + @moduledoc ~S""" Functors provide a way to apply a function to value(s) a datatype (lists, trees, maybes, etc). @@ -42,16 +43,14 @@ defprotocol Witchcraft.Functor do iex> [1,2,3] |> Witchcraft.Functor.lift &(&1 + 1) [2,3,4] - iex> defimpl Witchcraft.Functor, for: Witchcraft.ADT.Id do - iex> def lift(%Witchcraft.ADT.Id{id: inner}, func), do: %Witchcraft.ADT.Id{id: func.(inner)} + 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> Witchcraft.Functor.lift(%Witchcraft.ADT.Id{id: 1}, &(&1 + 1)) - %Witchcraft.ADT.Id{id: 2} + iex> Witchcraft.Functor.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). @@ -60,12 +59,12 @@ defprotocol Witchcraft.Functor do def lift(data, function) end -defimpl Witchcraft.Functor, for: Any do +defimpl Witchcraft.Functor, for: Enum.t do @doc "Default implementation of `Functor` is `Enum.map`" def lift(data, func), do: Enum.map(data, func) end -defimpl Witchcraft.Functor, for: Witchcraft.ADT.Id do +defimpl Witchcraft.Functor, for: Witchcraft.Id do @doc "Example struct implimentation" - def lift(%Witchcraft.ADT.Id{id: data}, func), do: %Witchcraft.ADT.Id{id: func.(data)} + 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..b902144 --- /dev/null +++ b/lib/witchcraft/functor/function.ex @@ -0,0 +1,19 @@ +defmodule Witchcraft.Functor.Function do + @moduledoc ~S""" + """ + + import Witchcraft.Functor, only: [lift: 2] + + @spec lift((... -> any)) :: (any -> any) + def lift(fun), do: &lift(&1, fun) + + @doc ~S""" + Replace all of the input's data nodes with some constant value + + 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: a |> lift &Quark.const(&1, constant) +end diff --git a/lib/witchcraft/functor/functions.ex b/lib/witchcraft/functor/functions.ex deleted file mode 100644 index 9beae3a..0000000 --- a/lib/witchcraft/functor/functions.ex +++ /dev/null @@ -1,42 +0,0 @@ -defmodule Witchcraft.Functor.Functions do - @moduledoc ~S""" - """ - - alias Quark, as: Q - alias Witchcraft.Functor, as: F - - @spec lift((... -> any)) :: (any -> any) - def lift(fun), do: &lift(&1, fun) - - @doc ~S""" - Replace all of the input's data nodes with some constant value - - 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, &(Q.const(&1, constant))) - end - - @doc ~S""" - Alias for `lift` with arguments flipped ('map over') - - 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` - - 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..6f64372 --- /dev/null +++ b/lib/witchcraft/functor/operator.ex @@ -0,0 +1,21 @@ +defmodule Witchcraft.Functor.Operator do + @doc ~S""" + Alias for `lift` with arguments flipped ('map over') + + 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` + + 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 96% rename from lib/witchcraft/functor/properties.ex rename to lib/witchcraft/functor/property.ex index 907b656..d480cda 100644 --- a/lib/witchcraft/functor/properties.ex +++ b/lib/witchcraft/functor/property.ex @@ -1,4 +1,4 @@ -defmodule Witchcraft.Functor.Properties do +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. @@ -7,7 +7,7 @@ defmodule Witchcraft.Functor.Properties do import Quark, only: [id: 1] import Witchcraft.Functor - import Witchcraft.Functor.Functions + import Witchcraft.Functor.Function @doc ~S""" Check that lifting a function into some context returns a member of the target type diff --git a/lib/witchcraft/id.ex b/lib/witchcraft/id.ex new file mode 100644 index 0000000..287e13a --- /dev/null +++ b/lib/witchcraft/id.ex @@ -0,0 +1,10 @@ +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] +end diff --git a/lib/witchcraft/monoid.ex b/lib/witchcraft/monoid.ex index 1c65a0b..90f74a0 100644 --- a/lib/witchcraft/monoid.ex +++ b/lib/witchcraft/monoid.ex @@ -52,8 +52,6 @@ defprotocol Witchcraft.Monoid do For the protocol to operate as intended, you need to respect the above properties. """ - @fallback_to_any true - @doc "Get the identity ('zero') element of the monoid by passing in any element of the set" @spec identity(any) :: any def identity(a) 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..ae3e38a --- /dev/null +++ b/lib/witchcraft/monoid/operator.ex @@ -0,0 +1,42 @@ +defmodule Witchcraft.Monoid.Operator do + import Kernel, except: [<>: 2] + import Witchcraft.Monoid + + defmacro __using__(_) do + quote do + import Kernel, except: [<>: 2] + import Witchcraft.Monoid.Operator, only: [<>: 2] + end + end + + @doc ~S""" + Infix variant of `Monoid.append` + + # Example + + ```elixir + + ...> defimpl 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 + + ...> defimpl Monoid, for: List do + ...> def identity(_), do: [] + ...> def append(a, b), do: a ++ b + ...> end + 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 73% rename from lib/witchcraft/monoid/properties.ex rename to lib/witchcraft/monoid/property.ex index 1ff234a..362ce1c 100644 --- a/lib/witchcraft/monoid/properties.ex +++ b/lib/witchcraft/monoid/property.ex @@ -5,24 +5,24 @@ defmodule Witchcraft.Monoid.Properties do They are placed here as a quick way to spotcheck some of your values. """ - import Witchcraft.Monoid - import Witchcraft.Monoid.Functions + use Witchcraft.Monoid.Operator + import Witchcraft.Monoid, only: [identity: 1, append: 2] @doc """ Check that some member of your monoid combines with the identity to return itself """ @spec spotcheck_identity(any) :: boolean def spotcheck_identity(member) do - (identity(member) <|> member) == member + (identity(member) <> member) == member end @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) """ @spec spotcheck_associativity(any, any, any) :: boolean def spotcheck_associativity(member1, member2, member3) do - (member1 <|> (member2 <|> member3)) == ((member1 <|> member2) <|> member3) + (member1 <> (member2 <> member3)) == ((member1 <> member2) <> member3) end @doc """ diff --git a/test/functor_test.exs b/test/functor_test.exs index b95a9f9..7ff28ae 100644 --- a/test/functor_test.exs +++ b/test/functor_test.exs @@ -1,11 +1,11 @@ -defmodule FunctorTest do - use ExUnit.Case +# defmodule FunctorTest do +# use ExUnit.Case - import Witchcraft.Functor - import Witchcraft.Functor.Functions - import Witchcraft.Functor.Properties +# import Witchcraft.Functor +# import Witchcraft.Functor.Functions +# import Witchcraft.Functor.Properties - doctest Witchcraft.Functor - doctest Witchcraft.Functor.Functions - doctest Witchcraft.Functor.Properties -end +# doctest Witchcraft.Functor +# doctest Witchcraft.Functor.Functions +# doctest Witchcraft.Functor.Properties +# end diff --git a/test/witchcraft_test.exs b/test/witchcraft_test.exs index 8ce47c0..2eb4b59 100644 --- a/test/witchcraft_test.exs +++ b/test/witchcraft_test.exs @@ -2,4 +2,6 @@ defmodule WitchcraftTest do use ExUnit.Case doctest Witchcraft.Monoid, import: true + doctest Witchcraft.Monoid.Operator, import: true + doctest Witchcraft.Monoid.Property, import: true end From 2468e7adeaa7c3f10c5b9f4aa448c0900ad027aa Mon Sep 17 00:00:00 2001 From: Brooklyn Zelenka Date: Sat, 9 Jan 2016 12:19:39 -0800 Subject: [PATCH 07/10] Update deps --- lib/witchcraft/applicative.ex | 83 -------------------------- lib/witchcraft/applicative/function.ex | 28 --------- lib/witchcraft/applicative/operator.ex | 22 ------- lib/witchcraft/applicative/property.ex | 71 ---------------------- lib/witchcraft/functor.ex | 14 ++--- lib/witchcraft/functor/function.ex | 33 ++++++++-- lib/witchcraft/functor/operator.ex | 12 +++- lib/witchcraft/functor/property.ex | 42 +++++++++---- lib/witchcraft/id.ex | 3 + lib/witchcraft/monoid.ex | 4 +- lib/witchcraft/monoid/operator.ex | 16 +++-- lib/witchcraft/monoid/property.ex | 4 +- mix.exs | 27 +++------ mix.lock | 6 +- test/functor_test.exs | 11 ---- test/monoid_test.exs | 56 ----------------- test/witchcraft/monoid_test.exs | 56 +++++++++++++++++ test/witchcraft_test.exs | 5 ++ 18 files changed, 165 insertions(+), 328 deletions(-) delete mode 100644 lib/witchcraft/applicative.ex delete mode 100644 lib/witchcraft/applicative/function.ex delete mode 100644 lib/witchcraft/applicative/operator.ex delete mode 100644 lib/witchcraft/applicative/property.ex delete mode 100644 test/functor_test.exs delete mode 100644 test/monoid_test.exs create mode 100644 test/witchcraft/monoid_test.exs diff --git a/lib/witchcraft/applicative.ex b/lib/witchcraft/applicative.ex deleted file mode 100644 index df988a6..0000000 --- a/lib/witchcraft/applicative.ex +++ /dev/null @@ -1,83 +0,0 @@ -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` - - iex> apply(v, wrap(&id(&1))) == v - True - - ## Composition - `apply` composes normally. - - iex> apply((wrap &compose(&1,&2)),(apply(u,(apply(v, w))))) == apply(u,(apply(v, w))) - True - - ## 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. - - iex> apply(wrap x, wrap f) == wrap f(x)) - True - - ## Interchange - The order does not matter when `apply`ing to a `wrap`ped value - and a `wrap`ped function. - - iex> apply(wrap y, u) == apply(u, wrap &(lift(y, &1)) - True - - ## Functor - Being an applicative _functor_, `apply` behaves as `lift` on `wrap`ped values - - iex> lift(x, f) == apply(x, (wrap f)) - True - - # Notes - Given that Elixir functons are right-associative, you can write clean looking, - but much more ambiguous versions: - - iex> wrap y |> apply u == u |> apply wrap &lift(y, &1) - True - - iex> x |> lift f == x |> apply wrap f - True - - However, it is strongly recommended to include the parentheses for clarity. - - """ - - require Witchcraft.Id - - @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: Quary.curry(bare_function).(bare_value) -end diff --git a/lib/witchcraft/applicative/function.ex b/lib/witchcraft/applicative/function.ex deleted file mode 100644 index 6aa82e0..0000000 --- a/lib/witchcraft/applicative/function.ex +++ /dev/null @@ -1,28 +0,0 @@ -defmodule Witchcraft.Applicative.Function do - @moduledoc ~S""" - Function helpers, derivatives and operators for `Witchcraft.Applicative` - """ - - import Witchcraft.Functor.Operator - - @doc ~S""" - `lift` a function that takes a list of arguments - - 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], (... -> a)) :: any - def lift([value|[]], func), do: value <~ func - - def lift([head|tail], func) do - lifted = Quark.Curry.curry(func) ~> head - Enum.reduce(tail, lifted, Quark.flip(apply)) - end -end diff --git a/lib/witchcraft/applicative/operator.ex b/lib/witchcraft/applicative/operator.ex deleted file mode 100644 index 5505b20..0000000 --- a/lib/witchcraft/applicative/operator.ex +++ /dev/null @@ -1,22 +0,0 @@ -defmodule Witchcraft.Applicative.Operator do - @doc ~S""" - Infix alias for `Witchcraft.Applicative.apply` - - iex> [1,2,3] <<~ [&(&1 + 1), &(&1 * 10)] - [2,3,4,10,20,30] - - """ - @spec any <<~ (any -> any) :: any - def value <<~ func, do: Witchcraft.Applicative.apply(value, func) - - @doc ~S""" - Infix alias for `Witchcraft.Applicative.apply`, with arguments reversed. - This allows for easier reading in an classic "appicative style". - - iex> [&(&1 + 1), &(&1 * 10)] ~>> [1,2,3] - [2,3,4,10,20,30] - - """ - @spec (any -> any) ~>> any :: any - def func ~>> value, do: value <<~ func -end diff --git a/lib/witchcraft/applicative/property.ex b/lib/witchcraft/applicative/property.ex deleted file mode 100644 index 4fe6194..0000000 --- a/lib/witchcraft/applicative/property.ex +++ /dev/null @@ -1,71 +0,0 @@ -defmoule Witchraft.Applicative.Property do - @moduledoc ~S""" - ## Composition - `apply` composes normally. - - ```elixir - - iex> apply((wrap &compose(&1,&2)),(apply(u,(apply(v, w))))) == apply(u,(apply(v, w))) - True - - ``` - - ## 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. - - ```elixir - - iex> apply(wrap x, wrap f) == wrap f(x) - True - - ``` - - ## Interchange - The order does not matter when `apply`ing to a `wrap`ped value - and a `wrap`ped function. - - ```elixir - - iex> apply(wrap y, u) == apply(u, wrap &(lift(y, &1)) - True - - ``` - - ## Functor - Being an applicative _functor_, `apply` behaves as `lift` on `wrap`ped values - - ```elixir - - iex> lift(x, f) == apply(x, (wrap f)) - True - - ``` - - # Notes - Given that Elixir functons are right-associative, you can write clean looking, - but much more ambiguous versions: - - ```elixir - - iex> wrap y |> apply u == u |> apply wrap &lift(y, &1) - True - - iex> x |> lift f == x |> apply wrap f - True - - ``` - - However, it is strongly recommended to include the parentheses for clarity. - """ - - import Quark, only: [id: 1] - import Witchcraft.Applicative - import Witchcraft.Applicative.Function - - @doc ~S""" - `apply`ing a lifted `id` to some lifted value `v` does not change `v` - """ - @spec spotcheck_applicative_identity() - def spotcheck_applicative_identity(value, do: value ~>> wrap id == value -end diff --git a/lib/witchcraft/functor.ex b/lib/witchcraft/functor.ex index 4062825..893b9a3 100644 --- a/lib/witchcraft/functor.ex +++ b/lib/witchcraft/functor.ex @@ -1,6 +1,4 @@ defprotocol Witchcraft.Functor do - require Witchcraft.Id - @moduledoc ~S""" Functors provide a way to apply a function to value(s) a datatype (lists, trees, maybes, etc). @@ -22,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. @@ -40,17 +38,19 @@ defprotocol Witchcraft.Functor do # Examples - iex> [1,2,3] |> Witchcraft.Functor.lift &(&1 + 1) + iex> [1,2,3] |> lift(&(&1 + 1)) [2,3,4] 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> Witchcraft.Functor.lift(%Witchcraft.Id{id: 1}, &(&1 + 1)) + 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). @@ -59,7 +59,7 @@ defprotocol Witchcraft.Functor do def lift(data, function) end -defimpl Witchcraft.Functor, for: Enum.t do +defimpl Witchcraft.Functor, for: Any do @doc "Default implementation of `Functor` is `Enum.map`" def lift(data, func), do: Enum.map(data, func) end diff --git a/lib/witchcraft/functor/function.ex b/lib/witchcraft/functor/function.ex index b902144..f5c9081 100644 --- a/lib/witchcraft/functor/function.ex +++ b/lib/witchcraft/functor/function.ex @@ -1,19 +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) - def lift(fun), do: &lift(&1, fun) + defpartial lift(fun), do: &lift(&1, fun) @doc ~S""" Replace all of the input's data nodes with some constant value - iex> Witchcraft.Functor.Functions.map_replace([1,2,3], 42) - [42, 42, 42] + ```elixir + + iex> [1,2,3] |> replace(42) + [42, 42, 42] + + ``` """ - @spec map_replace(any, any) :: any - def map_replace(a, constant), do: a |> lift &Quark.const(&1, constant) + @spec replace(any, any) :: any + def replace(functor, const), do: lift(functor, &Quark.constant(const, &1)) end diff --git a/lib/witchcraft/functor/operator.ex b/lib/witchcraft/functor/operator.ex index 6f64372..2b3c06b 100644 --- a/lib/witchcraft/functor/operator.ex +++ b/lib/witchcraft/functor/operator.ex @@ -1,20 +1,30 @@ defmodule Witchcraft.Functor.Operator do + import Witchcraft.Functor, only: [lift: 2] + @doc ~S""" Alias for `lift` with arguments flipped ('map over') + ```elixir + iex> (&(&1 * 10)) ~> [1,2,3] [10, 20, 30] + ``` + """ @spec (any -> any) ~> any :: any - def func ~> args, do: F.lift(args, func) + def func ~> args, do: lift(args, func) @doc ~S""" Alias for `lift` + ```elixir + iex> [1,2,3] <~ &(&1 * 10) [10, 20, 30] + ``` + """ @spec any <~ (any -> any) :: any def args <~ func, do: func ~> args diff --git a/lib/witchcraft/functor/property.ex b/lib/witchcraft/functor/property.ex index d480cda..4c3403f 100644 --- a/lib/witchcraft/functor/property.ex +++ b/lib/witchcraft/functor/property.ex @@ -5,16 +5,19 @@ defmodule Witchcraft.Functor.Property do They are placed here as a quick way to spotcheck some of your values. """ - import Quark, only: [id: 1] - import Witchcraft.Functor - import Witchcraft.Functor.Function + 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 - iex> alias Witchcraft.ADT.Id, as: Id - iex> spotcheck_associates_object(%Id{id: 42}, &(&1), &Id.is_id/1) - true + ```elixir + + iex> alias Witchcraft.Id, as: Id + iex> spotcheck_associates_object(%Id{id: 42}, &Quark.id/1, &Id.is_id/1) + true + + ``` """ @spec spotcheck_associates_object(any, (any -> any), (any -> boolean)) :: boolean @@ -36,8 +39,12 @@ defmodule Witchcraft.Functor.Property do B ---- id ----> B - iex> spotcheck_preserve_identity(%Witchcraft.ADT.Id{id: 7}, &(&1 + 1)) - true + ```elixir + + iex> spotcheck_preserve_identity(%Witchcraft.Id{id: 7}, &(&1 + 1)) + true + + ``` """ @spec spotcheck_preserve_identity(any, (any -> any)) :: boolean @@ -48,8 +55,13 @@ defmodule Witchcraft.Functor.Property do @doc ~S""" Check that lifting a composed function is the same as lifting functions in sequence - iex> spotcheck_preserve_compositon(%Witchcraft.ADT.Id{id: 5}, &(&1 + 1), &(&1 * 10)) - true + ```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 @@ -59,9 +71,13 @@ defmodule Witchcraft.Functor.Property do @doc ~S""" Spotcheck all functor properties - iex> alias Witchcraft.ADT.Id, as: Id - iex> spotcheck(%Id{id: 42}, &(&1 + 1), &(&1 * 2), &Id.is_id&1) - true + ```elixir + + iex> alias Witchcraft.Id, as: Id + iex> spotcheck(%Id{id: 42}, &(&1 + 1), &(&1 * 2), &Id.is_id&1) + true + + ``` """ @spec spotcheck(any, (any -> any), (any -> any), (any -> boolean)) :: boolean diff --git a/lib/witchcraft/id.ex b/lib/witchcraft/id.ex index 287e13a..195ac75 100644 --- a/lib/witchcraft/id.ex +++ b/lib/witchcraft/id.ex @@ -7,4 +7,7 @@ defmodule Witchcraft.Id do """ @type t :: %Witchcraft.Id{id: any} defstruct [:id] + + def is_id(%Witchcraft.Id{id: _}), do: true + def is_id(_), do: false end diff --git a/lib/witchcraft/monoid.ex b/lib/witchcraft/monoid.ex index 90f74a0..a9bd790 100644 --- a/lib/witchcraft/monoid.ex +++ b/lib/witchcraft/monoid.ex @@ -16,7 +16,7 @@ defprotocol Witchcraft.Monoid do # Examples ## Theory - ```elixir + ``` # Pseudocode identity = 0 @@ -38,7 +38,7 @@ defprotocol Witchcraft.Monoid do ...> def append(a, b), do: a + b ...> end iex> - iex> 1 |> op 4 |> op 2 |> op 10 + iex> 1 |> append(4) |> append(2) |> append(10) 17 ``` diff --git a/lib/witchcraft/monoid/operator.ex b/lib/witchcraft/monoid/operator.ex index ae3e38a..b23b837 100644 --- a/lib/witchcraft/monoid/operator.ex +++ b/lib/witchcraft/monoid/operator.ex @@ -1,6 +1,6 @@ defmodule Witchcraft.Monoid.Operator do import Kernel, except: [<>: 2] - import Witchcraft.Monoid + import Witchcraft.Monoid, only: [append: 2] defmacro __using__(_) do quote do @@ -16,21 +16,27 @@ defmodule Witchcraft.Monoid.Operator do ```elixir - ...> defimpl Monoid, for: Integer do + 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 + iex> 1 |> append(4) |> append(2) |> append(10) 17 + + iex> use Witchcraft.Monoid.Operator iex> 1 <> 4 <> 2 <> 10 17 - ...> defimpl Monoid, for: List do + iex> import Witchcraft.Monoid + ...> defimpl Witchcraft.Monoid, for: List do ...> def identity(_), do: [] ...> def append(a, b), do: a ++ b ...> end - iex> 1 |> append 4 |> append 2 |> append 10 + iex> 1 |> append(4) |> append(2) |> append(10) 17 + + iex> use Witchcraft.Monoid.Operator iex> [42, 43] <> [44] <> [45, 46] <> [47] [42, 43, 44, 45, 46, 47] diff --git a/lib/witchcraft/monoid/property.ex b/lib/witchcraft/monoid/property.ex index 362ce1c..722bce1 100644 --- a/lib/witchcraft/monoid/property.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,7 +6,7 @@ defmodule Witchcraft.Monoid.Properties do """ use Witchcraft.Monoid.Operator - import Witchcraft.Monoid, only: [identity: 1, append: 2] + import Witchcraft.Monoid @doc """ Check that some member of your monoid combines with the identity to return itself diff --git a/mix.exs b/mix.exs index 683bd46..2ddb29e 100644 --- a/mix.exs +++ b/mix.exs @@ -14,34 +14,23 @@ defmodule Witchcraft.Mixfile do 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}, - {:quark, "~> 1.0", only: :dev}] + {:quark, "~> 1.0"}] end defp package do diff --git a/mix.lock b/mix.lock index 49120e7..6dcb5da 100644 --- a/mix.lock +++ b/mix.lock @@ -1,4 +1,4 @@ %{"curry": {:hex, :curry, "0.0.1"}, - "earmark": {:hex, :earmark, "0.1.17"}, - "ex_doc": {:hex, :ex_doc, "0.10.0"}, - "quark": {:hex, :quark, "1.0.0"}} + "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 7ff28ae..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/witchcraft/monoid_test.exs b/test/witchcraft/monoid_test.exs new file mode 100644 index 0000000..be4be35 --- /dev/null +++ b/test/witchcraft/monoid_test.exs @@ -0,0 +1,56 @@ +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/witchcraft_test.exs b/test/witchcraft_test.exs index 2eb4b59..dcae80e 100644 --- a/test/witchcraft_test.exs +++ b/test/witchcraft_test.exs @@ -4,4 +4,9 @@ defmodule WitchcraftTest do doctest Witchcraft.Monoid, import: true doctest Witchcraft.Monoid.Operator, import: true doctest Witchcraft.Monoid.Property, import: true + + doctest Witchcraft.Functor, import: true + doctest Witchcraft.Functor.Function, import: true + doctest Witchcraft.Functor.Operator, import: true + doctest Witchcraft.Functor.Property, import: true end From 362feb49b91bc6d4965de58d2dfabc3cce9df304 Mon Sep 17 00:00:00 2001 From: Brooklyn Zelenka Date: Sat, 9 Jan 2016 15:22:53 -0800 Subject: [PATCH 08/10] defimpl Applicative List and Id --- lib/witchcraft/applicative.ex | 98 ++++++++++++++++++++++++++ lib/witchcraft/applicative/function.ex | 49 +++++++++++++ lib/witchcraft/applicative/operator.ex | 37 ++++++++++ lib/witchcraft/applicative/property.ex | 72 +++++++++++++++++++ test/witchcraft_test.exs | 5 ++ 5 files changed, 261 insertions(+) create mode 100644 lib/witchcraft/applicative.ex create mode 100644 lib/witchcraft/applicative/function.ex create mode 100644 lib/witchcraft/applicative/operator.ex create mode 100644 lib/witchcraft/applicative/property.ex diff --git a/lib/witchcraft/applicative.ex b/lib/witchcraft/applicative.ex new file mode 100644 index 0000000..b88706c --- /dev/null +++ b/lib/witchcraft/applicative.ex @@ -0,0 +1,98 @@ +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` + + # iex> apply(v, wrap(&id(&1))) == v + # True + + ## Composition + `apply` composes normally. + + # iex> apply((wrap &compose(&1,&2)), (apply(u,(apply(v, w))))) == apply(u,(apply(v, w))) + # True + + ## 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. + + # iex> apply(wrap x, wrap f) == wrap f(x)) + # True + + ## Interchange + The order does not matter when `apply`ing to a `wrap`ped value + and a `wrap`ped function. + + # iex> apply(wrap y, u) == apply(u, wrap &(lift(y, &1)) + # True + + ## Functor + Being an applicative _functor_, `apply` behaves as `lift` on `wrap`ped values + + # iex> lift(x, f) == apply(x, (wrap f)) + # True + + # Notes + Given that Elixir functons are right-associative, you can write clean looking, + but much more ambiguous versions: + + # iex> wrap(y) |> apply(u) == apply(u, wrap(&lift(y, &1))) + # True + + # iex> lift(x, f) == apply(x, wrap f) + # True + + 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] + + def wrap(_, bare), do: [bare] + def apply(values, functions), do: Enum.reduce(functions, [], &helper(&1, values, &2)) + + defp helper(acc, fun, vals), do: acc ++ Enum.map(vals, curry(fun)) +end + +defimpl Witchcraft.Applicative, for: Witchcraft.Id do + import Quark.Curry, only: [curry: 1] + alias Witchcraft.Id, as: Id + + def wrap(_, bare), do: %Witchcraft.Id{id: bare} + 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..dff95a3 --- /dev/null +++ b/lib/witchcraft/applicative/function.ex @@ -0,0 +1,49 @@ +defmodule Witchcraft.Applicative.Function do + @moduledoc ~S""" + Function helpers, derivatives and operators for `Witchcraft.Applicative` + """ + + import Kernel, except: [apply: 2] + + import Quark, only: [flip: 1] + import Quark.Curry, only: [curry: 1] + + import Witchcraft.Applicative, only: [apply: 2] + import Witchcraft.Functor.Operator, only: [<~: 2, ~>: 2] + + defmacro __using__(_) do + quote do + import Kernel, except: [apply: 2] + import Witchcraft.Applicative.Function, only: [lift: 2] + end + end + + + # liftA* + @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], func), do: value <~ func + + def lift([head|tail], func) do + lifted = curry(func) ~> head + Enum.reduce(tail, lifted, flip(&apply/2)) + end + + defdelegate lift(functor_value, bare_function), to: Witchcraft.Functor +end diff --git a/lib/witchcraft/applicative/operator.ex b/lib/witchcraft/applicative/operator.ex new file mode 100644 index 0000000..a2e4cc4 --- /dev/null +++ b/lib/witchcraft/applicative/operator.ex @@ -0,0 +1,37 @@ +defmodule Witchcraft.Applicative.Operator do + @moduledoc ~S""" + """ + + import Kernel, except: [apply: 2] + import Witchcraft.Applicative, only: [apply: 2] + + @doc ~S""" + Infix alias for `Witchcraft.Applicative.apply` + + ```elixir + + iex> + iex> [1,2,3] <<~ [&(&1 + 1), &(&1 * 10)] + [2,3,4,10,20,30] + + ``` + + """ + @spec any <<~ (any -> any) :: any + def value <<~ func, do: apply(value, func) + + @doc ~S""" + Infix alias for `Witchcraft.Applicative.apply`, with arguments reversed. + This allows for easier reading in an classic "appicative style". + + ```elixir + + iex> [&(&1 + 1), &(&1 * 10)] ~>> [1,2,3] + [2,3,4,10,20,30] + + ``` + + """ + @spec (any -> any) ~>> any :: any + def func ~>> value, do: value <<~ func +end diff --git a/lib/witchcraft/applicative/property.ex b/lib/witchcraft/applicative/property.ex new file mode 100644 index 0000000..ec75a09 --- /dev/null +++ b/lib/witchcraft/applicative/property.ex @@ -0,0 +1,72 @@ +defmodule Witchraft.Applicative.Property do + @moduledoc ~S""" + ## Composition + `apply` composes normally. + + ```elixir + + iex> apply((wrap &compose(&1,&2)),(apply(u,(apply(v, w))))) == apply(u,(apply(v, w))) + True + + ``` + + ## 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. + + ```elixir + + iex> apply(wrap x, wrap f) == wrap f(x) + True + + ``` + + ## Interchange + The order does not matter when `apply`ing to a `wrap`ped value + and a `wrap`ped function. + + ```elixir + + iex> apply(wrap y, u) == apply(u, wrap &(lift(y, &1)) + True + + ``` + + ## Functor + Being an applicative _functor_, `apply` behaves as `lift` on `wrap`ped values + + ```elixir + + iex> lift(x, f) == apply(x, (wrap f)) + True + + ``` + + # Notes + Given that Elixir functons are right-associative, you can write clean looking, + but much more ambiguous versions: + + ```elixir + + iex> wrap y |> apply u == u |> apply wrap &lift(y, &1) + True + + iex> x |> lift f == x |> apply wrap f + True + + ``` + + However, it is strongly recommended to include the parentheses for clarity. + """ + + import Quark, only: [id: 1] + import Witchcraft.Applicative.Operator, only: [<<~: 2] + + alias Witchcraft.Applicative, as: A + + @doc ~S""" + `apply`ing a lifted `id` to some lifted value `v` does not change `v` + """ + @spec spotcheck_applicative_identity(any) :: boolean + def spotcheck_applicative_identity(value), do: value <<~ fn x -> A.wrap(x, &id/1) end == value +end diff --git a/test/witchcraft_test.exs b/test/witchcraft_test.exs index dcae80e..f8db32a 100644 --- a/test/witchcraft_test.exs +++ b/test/witchcraft_test.exs @@ -9,4 +9,9 @@ defmodule WitchcraftTest do doctest Witchcraft.Functor.Function, import: true doctest Witchcraft.Functor.Operator, import: true doctest Witchcraft.Functor.Property, import: true + + doctest Witchcraft.Applicative, import: true + doctest Witchcraft.Applicative.Function, import: true + doctest Witchcraft.Applicative.Operator, import: true + # doctest Witchcraft.Applicative.Property, import: true end From 1efe02e668529c4d80fcc0e7e3260afa2334b6b3 Mon Sep 17 00:00:00 2001 From: Brooklyn Zelenka Date: Sat, 9 Jan 2016 17:11:09 -0800 Subject: [PATCH 09/10] Reverse all arrow directions to match data flow --- lib/witchcraft/applicative.ex | 80 ++++++++++++++---- lib/witchcraft/applicative/function.ex | 27 ++---- lib/witchcraft/applicative/operator.ex | 31 +++++-- lib/witchcraft/applicative/property.ex | 110 +++++++++++++++---------- lib/witchcraft/functor.ex | 26 ++++-- lib/witchcraft/functor/operator.ex | 18 ++-- lib/witchcraft/id.ex | 9 ++ lib/witchcraft/monoid.ex | 90 ++++++++++++++++---- lib/witchcraft/monoid/operator.ex | 22 +---- lib/witchcraft/monoid/property.ex | 46 +++++++++-- test/witchcraft/monoid_test.exs | 56 ------------- test/witchcraft_test.exs | 23 +++++- 12 files changed, 338 insertions(+), 200 deletions(-) delete mode 100644 test/witchcraft/monoid_test.exs diff --git a/lib/witchcraft/applicative.ex b/lib/witchcraft/applicative.ex index b88706c..9a56fd4 100644 --- a/lib/witchcraft/applicative.ex +++ b/lib/witchcraft/applicative.ex @@ -9,44 +9,37 @@ defprotocol Witchcraft.Applicative do ## Identity `apply`ing a lifted `id` to some lifted value `v` does not change `v` - # iex> apply(v, wrap(&id(&1))) == v - # True + `apply(v, wrap(&id(&1))) == v` ## Composition `apply` composes normally. - # iex> apply((wrap &compose(&1,&2)), (apply(u,(apply(v, w))))) == apply(u,(apply(v, w))) - # True + `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. - # iex> apply(wrap x, wrap f) == wrap f(x)) - # True + `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. - # iex> apply(wrap y, u) == apply(u, wrap &(lift(y, &1)) - # True + `apply(wrap y, u) == apply(u, wrap &(lift(y, &1))` ## Functor Being an applicative _functor_, `apply` behaves as `lift` on `wrap`ped values - # iex> lift(x, f) == apply(x, (wrap f)) - # True + `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: - # iex> wrap(y) |> apply(u) == apply(u, wrap(&lift(y, &1))) - # True + `wrap(y) |> apply(u) == apply(u, wrap(&lift(y, &1)))` - # iex> lift(x, f) == apply(x, wrap f) - # True + `lift(x, f) == apply(x, wrap f)` However, it is strongly recommended to include the parentheses for clarity. @@ -83,16 +76,71 @@ 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] - def apply(values, functions), do: Enum.reduce(functions, [], &helper(&1, values, &2)) - defp helper(acc, fun, vals), do: acc ++ Enum.map(vals, curry(fun)) + @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 index dff95a3..5f39c19 100644 --- a/lib/witchcraft/applicative/function.ex +++ b/lib/witchcraft/applicative/function.ex @@ -11,39 +11,24 @@ defmodule Witchcraft.Applicative.Function do import Witchcraft.Applicative, only: [apply: 2] import Witchcraft.Functor.Operator, only: [<~: 2, ~>: 2] - defmacro __using__(_) do - quote do - import Kernel, except: [apply: 2] - import Witchcraft.Applicative.Function, only: [lift: 2] - end - end - - - # liftA* @doc ~S""" `lift` a function that takes a list of arguments ```elixir - iex> lift([1,2,3], [4,5,6], &(&1 + &2)) + 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)) + 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)) + 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], func), do: value <~ func - - def lift([head|tail], func) do - lifted = curry(func) ~> head - Enum.reduce(tail, lifted, flip(&apply/2)) - end - - defdelegate lift(functor_value, bare_function), to: Witchcraft.Functor + @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) end diff --git a/lib/witchcraft/applicative/operator.ex b/lib/witchcraft/applicative/operator.ex index a2e4cc4..af96278 100644 --- a/lib/witchcraft/applicative/operator.ex +++ b/lib/witchcraft/applicative/operator.ex @@ -4,34 +4,47 @@ defmodule Witchcraft.Applicative.Operator do 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` + Infix alias for `Witchcraft.Applicative.apply`. If chaining, be sure to wrap + each layer in parentheses, as `~>>` and `~>` are left associative. ```elixir - iex> - iex> [1,2,3] <<~ [&(&1 + 1), &(&1 * 10)] + 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) :: any - def value <<~ func, do: apply(value, func) + @spec any ~>> any :: any + def value ~>> func, do: apply(value, func) @doc ~S""" Infix alias for `Witchcraft.Applicative.apply`, with arguments reversed. - This allows for easier reading in an classic "appicative style". + + 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] + 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 :: any - def func ~>> value, do: value <<~ func + @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 index ec75a09..a4784d4 100644 --- a/lib/witchcraft/applicative/property.ex +++ b/lib/witchcraft/applicative/property.ex @@ -1,72 +1,98 @@ -defmodule Witchraft.Applicative.Property do +defmodule Witchcraft.Applicative.Property do @moduledoc ~S""" - ## Composition - `apply` composes normally. + 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`. - ```elixir + They are placed here as a quick way to spotcheck some of your values. + """ - iex> apply((wrap &compose(&1,&2)),(apply(u,(apply(v, w))))) == apply(u,(apply(v, w))) - True + import Kernel, except: [apply: 2] - ``` + import Quark, only: [compose: 2, id: 1] + import Quark.Curry, only: [curry: 1] - ## 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. + 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> apply(wrap x, wrap f) == wrap f(x) - True + iex> spotcheck_identity [] + true + + iex> spotcheck_identity %Witchcraft.Id{} + true ``` - ## Interchange - The order does not matter when `apply`ing to a `wrap`ped value - and a `wrap`ped function. + """ + @spec spotcheck_identity(any) :: boolean + def spotcheck_identity(value), do: (value ~>> wrap(value, &id/1)) == value - ```elixir + # @doc ~S""" + # `apply` composes normally. - iex> apply(wrap y, u) == apply(u, wrap &(lift(y, &1)) - True + # iex> spotcheck_composition([1, 2], [&(&1)], [&(&1 * 2)], [&(&1 * 10)]) + # true - ``` - - ## Functor - Being an applicative _functor_, `apply` behaves as `lift` on `wrap`ped values + # """ + # # @spec + # def spotcheck_composition(x, u, v, w) do + # x ~>> (wrap(u, &compose/2) <<~ u <<~ v <<~ w) == x ~>> u ~>> (v ~>> w) + # 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> lift(x, f) == apply(x, (wrap f)) - True + 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 - # Notes - Given that Elixir functons are right-associative, you can write clean looking, - but much more ambiguous versions: + # @doc ~S""" + # The order does not matter when `apply`ing to a `wrap`ped value + # and a `wrap`ped function. - ```elixir + # ```elixir - iex> wrap y |> apply u == u |> apply wrap &lift(y, &1) - True + # iex> spotcheck_interchange(1, [&(&1 * 10)]) + # true - iex> x |> lift f == x |> apply wrap f - 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 |> &1)) + # end - ``` + @doc ~S""" - However, it is strongly recommended to include the parentheses for clarity. - """ + Being an applicative _functor_, `apply` behaves as `lift` on `wrap`ped values - import Quark, only: [id: 1] - import Witchcraft.Applicative.Operator, only: [<<~: 2] + ```elixir - alias Witchcraft.Applicative, as: A + iex> spotcheck_functor([1,2,3], &(&1 * 10)) + true + + iex> spotcheck_functor(%Witchcraft.Id{id: 7}, &(&1 * 99)) + true + + ``` - @doc ~S""" - `apply`ing a lifted `id` to some lifted value `v` does not change `v` """ - @spec spotcheck_applicative_identity(any) :: boolean - def spotcheck_applicative_identity(value), do: value <<~ fn x -> A.wrap(x, &id/1) end == value + @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 893b9a3..b6fd6fd 100644 --- a/lib/witchcraft/functor.ex +++ b/lib/witchcraft/functor.ex @@ -49,8 +49,6 @@ defprotocol Witchcraft.Functor do """ - @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). @@ -59,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.Id do - @doc "Example struct implimentation" + @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/operator.ex b/lib/witchcraft/functor/operator.ex index 2b3c06b..490a16f 100644 --- a/lib/witchcraft/functor/operator.ex +++ b/lib/witchcraft/functor/operator.ex @@ -2,30 +2,32 @@ defmodule Witchcraft.Functor.Operator do import Witchcraft.Functor, only: [lift: 2] @doc ~S""" - Alias for `lift` with arguments flipped ('map over') + 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] + iex> (&(&1 * 10)) <~ [1,2,3] [10, 20, 30] ``` """ - @spec (any -> any) ~> any :: any - def func ~> args, do: lift(args, func) + @spec (any -> any) <~ any :: any + def func <~ args, do: lift(args, func) @doc ~S""" - Alias for `lift` + Alias for `lift` and `<~`, but with data flowing to the right. ```elixir - iex> [1,2,3] <~ &(&1 * 10) + iex> [1,2,3] ~> &(&1 * 10) [10, 20, 30] ``` """ - @spec any <~ (any -> any) :: any - def args <~ func, do: func ~> args + @spec any ~> (any -> any) :: any + def args ~> func, do: func <~ args end diff --git a/lib/witchcraft/id.ex b/lib/witchcraft/id.ex index 195ac75..e8d67c7 100644 --- a/lib/witchcraft/id.ex +++ b/lib/witchcraft/id.ex @@ -11,3 +11,12 @@ defmodule Witchcraft.Id do 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 a9bd790..2f3469e 100644 --- a/lib/witchcraft/monoid.ex +++ b/lib/witchcraft/monoid.ex @@ -13,12 +13,8 @@ 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 append(34, identity) == 34 @@ -29,20 +25,6 @@ defprotocol Witchcraft.Monoid do ``` - ## Concrete - - ```elixir - - iex> defimpl Witchcraft.Monoid, for: Integer do - ...> def identity(_), do: 0 - ...> def append(a, b), do: a + b - ...> end - iex> - iex> 1 |> append(4) |> append(2) |> append(10) - 17 - - ``` - ## Counter-Example Integer division is not a monoid. Because you cannot divide by zero, the property does not hold for all values in the set. @@ -62,15 +44,87 @@ defprotocol Witchcraft.Monoid do 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 diff --git a/lib/witchcraft/monoid/operator.ex b/lib/witchcraft/monoid/operator.ex index b23b837..4eef147 100644 --- a/lib/witchcraft/monoid/operator.ex +++ b/lib/witchcraft/monoid/operator.ex @@ -1,14 +1,6 @@ defmodule Witchcraft.Monoid.Operator do - import Kernel, except: [<>: 2] import Witchcraft.Monoid, only: [append: 2] - defmacro __using__(_) do - quote do - import Kernel, except: [<>: 2] - import Witchcraft.Monoid.Operator, only: [<>: 2] - end - end - @doc ~S""" Infix variant of `Monoid.append` @@ -24,25 +16,19 @@ defmodule Witchcraft.Monoid.Operator do iex> 1 |> append(4) |> append(2) |> append(10) 17 - iex> use Witchcraft.Monoid.Operator - iex> 1 <> 4 <> 2 <> 10 + iex> 1 <|> 4 <|> 2 <|> 10 17 iex> import Witchcraft.Monoid - ...> defimpl Witchcraft.Monoid, for: List do - ...> def identity(_), do: [] - ...> def append(a, b), do: a ++ b - ...> end iex> 1 |> append(4) |> append(2) |> append(10) 17 - iex> use Witchcraft.Monoid.Operator - iex> [42, 43] <> [44] <> [45, 46] <> [47] + iex> [42, 43] <|> [44] <|> [45, 46] <|> [47] [42, 43, 44, 45, 46, 47] ``` """ - @spec any <> any :: any - def a <> b, do: append(a, b) + @spec any <|> any :: any + def a <|> b, do: append(a, b) end diff --git a/lib/witchcraft/monoid/property.ex b/lib/witchcraft/monoid/property.ex index 722bce1..29b7622 100644 --- a/lib/witchcraft/monoid/property.ex +++ b/lib/witchcraft/monoid/property.ex @@ -5,28 +5,62 @@ defmodule Witchcraft.Monoid.Property do They are placed here as a quick way to spotcheck some of your values. """ - use Witchcraft.Monoid.Operator import Witchcraft.Monoid + 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.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 - (member1 <> (member2 <> member3)) == ((member1 <> member2) <> member3) + (member1 <|> (member2 <|> member3)) == ((member1 <|> member2) <|> member3) end @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/test/witchcraft/monoid_test.exs b/test/witchcraft/monoid_test.exs deleted file mode 100644 index be4be35..0000000 --- a/test/witchcraft/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/witchcraft_test.exs b/test/witchcraft_test.exs index f8db32a..7554352 100644 --- a/test/witchcraft_test.exs +++ b/test/witchcraft_test.exs @@ -1,17 +1,38 @@ defmodule WitchcraftTest do use ExUnit.Case + # 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 + doctest Witchcraft.Applicative.Property, import: true end From c6fa1ac0d42c557b885c519635138b98db916950 Mon Sep 17 00:00:00 2001 From: Brooklyn Zelenka Date: Tue, 12 Jan 2016 00:29:25 -0800 Subject: [PATCH 10/10] Spotchecks --- lib/witchcraft/applicative/function.ex | 14 +++++++- lib/witchcraft/applicative/property.ex | 45 ++++++++++++++------------ 2 files changed, 37 insertions(+), 22 deletions(-) diff --git a/lib/witchcraft/applicative/function.ex b/lib/witchcraft/applicative/function.ex index 5f39c19..39af1de 100644 --- a/lib/witchcraft/applicative/function.ex +++ b/lib/witchcraft/applicative/function.ex @@ -5,7 +5,7 @@ defmodule Witchcraft.Applicative.Function do import Kernel, except: [apply: 2] - import Quark, only: [flip: 1] + import Quark, only: [id: 1, flip: 1, constant: 2] import Quark.Curry, only: [curry: 1] import Witchcraft.Applicative, only: [apply: 2] @@ -31,4 +31,16 @@ defmodule Witchcraft.Applicative.Function do @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/property.ex b/lib/witchcraft/applicative/property.ex index a4784d4..77f43e0 100644 --- a/lib/witchcraft/applicative/property.ex +++ b/lib/witchcraft/applicative/property.ex @@ -32,17 +32,18 @@ defmodule Witchcraft.Applicative.Property do @spec spotcheck_identity(any) :: boolean def spotcheck_identity(value), do: (value ~>> wrap(value, &id/1)) == value - # @doc ~S""" - # `apply` composes normally. + @doc ~S""" + `apply` composes normally. + + iex> spotcheck_composition([1, 2], [&(&1 * 2)], [&(&1 * 10)]) + true - # iex> spotcheck_composition([1, 2], [&(&1)], [&(&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 - # """ - # # @spec - # def spotcheck_composition(x, u, v, w) do - # x ~>> (wrap(u, &compose/2) <<~ u <<~ v <<~ w) == x ~>> u ~>> (v ~>> w) - # 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. @@ -60,21 +61,23 @@ defmodule Witchcraft.Applicative.Property do 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. + @doc ~S""" + The order does not matter when `apply`ing to a `wrap`ped value + and a `wrap`ped function. + + ```elixir - # ```elixir + iex> spotcheck_interchange(1, [&(&1 * 10)]) + true - # 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 |> &1)) - # end + """ + @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"""