From ff37bcfd5290985d1c5c7fcec07cb3997e6128a5 Mon Sep 17 00:00:00 2001 From: sphaso Date: Sun, 15 Oct 2023 12:47:19 +0200 Subject: [PATCH] Checked algebraic laws, refactoring --- .github/workflows/elixir.yml | 2 +- lib/noether/either.ex | 6 ++--- lib/noether/maybe.ex | 20 +++++++------- mix.exs | 6 ++--- mix.lock | 8 +++--- test/noether/either_test.exs | 51 ++++++++++++++++++++++++++++++++++++ test/noether/maybe_test.exs | 19 ++++++++++++++ 7 files changed, 90 insertions(+), 22 deletions(-) diff --git a/.github/workflows/elixir.yml b/.github/workflows/elixir.yml index 50a16ff..2535c16 100644 --- a/.github/workflows/elixir.yml +++ b/.github/workflows/elixir.yml @@ -18,7 +18,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: Set up Elixir - uses: erlef/setup-beam@988e02bfe678367a02564f65ca2e37726dc0268f + uses: erlef/setup-beam@v1 with: elixir-version: '1.13' otp-version: '24.1' diff --git a/lib/noether/either.ex b/lib/noether/either.ex index eb93a65..65df52f 100644 --- a/lib/noether/either.ex +++ b/lib/noether/either.ex @@ -4,7 +4,7 @@ defmodule Noether.Either do These type of values will be then called `Either`. """ @type either :: {:ok, any()} | {:error, any()} - @type fun0 :: (() -> any()) + @type fun0 :: (-> any()) @type fun1 :: (any() -> any()) @doc """ @@ -60,8 +60,7 @@ defmodule Noether.Either do {:error, "Value not found"} """ @spec join(either()) :: either() - def join({:ok, {:ok, a}}), do: {:ok, a} - def join({:ok, {:error, a}}), do: {:error, a} + def join(a = {_, {_, _}}), do: bind(a, & &1) def join(a = {:error, _}), do: a @doc """ @@ -84,6 +83,7 @@ defmodule Noether.Either do @doc """ Given an `{:ok, value}` and a function that returns an Either value, it applies the function on the `value`. It effectively "squashes" an `{:ok, {:ok, v}}` or `{:ok, {:error, _}}` to its most appropriate representation. If an `{:error, _}` is given, it is returned as-is. + Please be careful and only use bind with functions that return either {:ok, _} or {:error, _}, otherwise you will break the Associativity law. ## Examples diff --git a/lib/noether/maybe.ex b/lib/noether/maybe.ex index ba13e83..5089b28 100644 --- a/lib/noether/maybe.ex +++ b/lib/noether/maybe.ex @@ -9,10 +9,10 @@ defmodule Noether.Maybe do ## Examples - iex> map(nil, &Kernel.abs/1) + iex> Noether.Maybe.map(nil, &Kernel.abs/1) nil - iex> map(-1, &Kernel.abs/1) + iex> Noether.Maybe.map(-1, &Kernel.abs/1) 1 """ @spec map(any(), fun1()) :: any() @@ -46,8 +46,12 @@ defmodule Noether.Maybe do :hello """ @spec maybe(any(), fun1(), any()) :: any() - def maybe(nil, _, default), do: default - def maybe(a, f, _) when is_function(f, 1), do: f.(a) + def maybe(a, f, default) when is_function(f, 1) do + case map(a, f) do + nil -> default + b -> b + end + end @doc """ Given a list of values, the function is mapped only on the elements different from `nil`. `nil` values will be discarded. A list of the results is returned. @@ -85,12 +89,6 @@ defmodule Noether.Maybe do """ @spec choose(any(), fun1(), fun1()) :: any() def choose(a, f, g) when is_function(f, 1) and is_function(g, 1) do - b = f.(a) - - if is_nil(b) do - g.(a) - else - b - end + maybe(a, f, map(a, g)) end end diff --git a/mix.exs b/mix.exs index ea3b2fa..989f86d 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule Noether.MixProject do def project do [ app: :noether, - version: "0.2.4", + version: "0.2.5", elixir: "~> 1.13", start_permanent: Mix.env() == :prod, aliases: aliases(), @@ -35,9 +35,9 @@ defmodule Noether.MixProject do # Run "mix help deps" to learn about dependencies. defp deps do [ - {:credo, "~> 1.6.4", only: :dev, runtime: false}, + {:credo, "~> 1.7", only: :dev, runtime: false}, {:ex_doc, "~> 0.28.4", only: :dev}, - {:dialyxir, "~> 1.1", only: :dev, runtime: false}, + {:dialyxir, "~> 1.3", only: :dev, runtime: false}, {:stream_data, "~> 0.5", only: :test} ] end diff --git a/mix.lock b/mix.lock index e3edf60..cf314af 100644 --- a/mix.lock +++ b/mix.lock @@ -1,12 +1,12 @@ %{ - "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"}, - "credo": {:hex, :credo, "1.6.4", "ddd474afb6e8c240313f3a7b0d025cc3213f0d171879429bf8535d7021d9ad78", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "c28f910b61e1ff829bffa056ef7293a8db50e87f2c57a9b5c3f57eee124536b7"}, - "dialyxir": {:hex, :dialyxir, "1.1.0", "c5aab0d6e71e5522e77beff7ba9e08f8e02bad90dfbeffae60eaf0cb47e29488", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "07ea8e49c45f15264ebe6d5b93799d4dd56a44036cf42d0ad9c960bc266c0b9a"}, + "bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"}, + "credo": {:hex, :credo, "1.7.1", "6e26bbcc9e22eefbff7e43188e69924e78818e2fe6282487d0703652bc20fd62", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "e9871c6095a4c0381c89b6aa98bc6260a8ba6addccf7f6a53da8849c748a58a2"}, + "dialyxir": {:hex, :dialyxir, "1.4.1", "a22ed1e7bd3a3e3f197b68d806ef66acb61ee8f57b3ac85fc5d57354c5482a93", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "84b795d6d7796297cca5a3118444b80c7d94f7ce247d49886e7c291e1ae49801"}, "earmark_parser": {:hex, :earmark_parser, "1.4.25", "2024618731c55ebfcc5439d756852ec4e85978a39d0d58593763924d9a15916f", [:mix], [], "hexpm", "56749c5e1c59447f7b7a23ddb235e4b3defe276afc220a6227237f3efe83f51e"}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, "ex_doc": {:hex, :ex_doc, "0.28.4", "001a0ea6beac2f810f1abc3dbf4b123e9593eaa5f00dd13ded024eae7c523298", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "bf85d003dd34911d89c8ddb8bda1a958af3471a274a4c2150a9c01c78ac3f8ed"}, "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, - "jason": {:hex, :jason, "1.3.0", "fa6b82a934feb176263ad2df0dbd91bf633d4a46ebfdffea0c8ae82953714946", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "53fc1f51255390e0ec7e50f9cb41e751c260d065dcba2bf0d08dc51a4002c2ac"}, + "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, "makeup_elixir": {:hex, :makeup_elixir, "0.16.0", "f8c570a0d33f8039513fbccaf7108c5d750f47d8defd44088371191b76492b0b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "28b2cbdc13960a46ae9a8858c4bebdec3c9a6d7b4b9e7f4ed1502f8159f338e7"}, "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, diff --git a/test/noether/either_test.exs b/test/noether/either_test.exs index 573b3f0..21835d9 100644 --- a/test/noether/either_test.exs +++ b/test/noether/either_test.exs @@ -1,4 +1,55 @@ defmodule Noether.EitherTest do use ExUnit.Case doctest Noether.Either, import: true + use ExUnitProperties + + alias Noether.Either + + describe "Functor laws" do + property "Identity" do + check all(n <- integer()) do + id = & &1 + el = {:ok, n} + elx = {:error, n} + assert el == Either.map(el, id) + assert elx == Either.map(elx, id) + assert el == Either.map_error(el, id) + assert elx == Either.map_error(elx, id) + end + end + + property "Composition" do + check all(n <- integer()) do + el = {:ok, n} + elx = {:error, n} + + assert el |> Either.map(&(&1 + 1)) |> Either.map(&(&1 * 2)) == + Either.map(el, &((&1 + 1) * 2)) + + assert elx |> Either.map_error(&(&1 + 1)) |> Either.map_error(&(&1 * 2)) == + Either.map_error(elx, &((&1 + 1) * 2)) + end + end + end + + describe "Monad laws" do + property "Left identity" do + check all(n <- integer()) do + assert Either.wrap(Either.bind({:ok, n}, &{:ok, &1 + 1})) == {:ok, n + 1} + end + end + + property "Right identity" do + check all(n <- integer()) do + assert Either.bind({:ok, n}, &Either.wrap/1) == {:ok, n} + end + end + + property "Associativity" do + check all(n <- integer()) do + assert Either.bind(Either.bind({:ok, n}, &{:ok, &1 + 1}), &{:ok, &1 * 2}) == + Either.bind({:ok, n}, &{:ok, (&1 + 1) * 2}) + end + end + end end diff --git a/test/noether/maybe_test.exs b/test/noether/maybe_test.exs index b328d63..1446990 100644 --- a/test/noether/maybe_test.exs +++ b/test/noether/maybe_test.exs @@ -1,4 +1,23 @@ defmodule Noether.MaybeTest do use ExUnit.Case doctest Noether.Maybe, import: true + use ExUnitProperties + + alias Noether.Maybe + + describe "Functor laws" do + property "Identity" do + check all(n <- integer()) do + id = & &1 + assert is_nil(Maybe.map(nil, id)) + assert n == Maybe.map(n, id) + end + end + + property "Composition" do + check all(n <- integer()) do + assert n |> Maybe.map(&(&1 + 1)) |> Maybe.map(&(&1 * 2)) == Maybe.map(n, &((&1 + 1) * 2)) + end + end + end end