├── .circleci └── config.yml ├── .envrc ├── .formatter.exs ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── config └── config.exs ├── default.nix ├── fe.png ├── lib ├── fe.ex └── fe │ ├── maybe.ex │ ├── result.ex │ └── review.ex ├── mix.exs ├── mix.lock └── test ├── fe_test.exs ├── maybe_test.exs ├── result_test.exs ├── review_test.exs └── test_helper.exs /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: circleci/elixir:1.7.3 6 | environment: 7 | MIX_ENV: test 8 | 9 | working_directory: ~/fe 10 | 11 | steps: 12 | - checkout 13 | 14 | - restore_cache: 15 | keys: 16 | - fe-plt-{{ checksum "mix.lock" }} 17 | 18 | - run: mix local.hex --force 19 | - run: mix local.rebar --force 20 | 21 | - run: mix deps.get 22 | - run: mix format --check-formatted 23 | - run: mix test 24 | 25 | - run: mix dialyzer 26 | - save_cache: 27 | key: fe-plt-{{ checksum "mix.lock" }} 28 | paths: 29 | - "_build/test" 30 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | use nix 2 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /_build/ 2 | /cover/ 3 | /deps/ 4 | /doc/ 5 | /.fetch 6 | erl_crash.dump 7 | *.ez 8 | fe-*.tar 9 | .direnv/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Distributed Owls 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: check deps format test 2 | 3 | check: test format 4 | mix dialyzer 5 | 6 | deps: 7 | mix deps.get 8 | 9 | format: 10 | mix format --check-formatted 11 | 12 | test: 13 | mix test 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Fe logo](fe.png) 2 | # Functional Elixir 3 | 4 | This library is a collection of useful data types brought to Elixir 5 | from other functional languages. 6 | 7 | ## Available Types 8 | 9 | Currently implemented types are: 10 | 11 | * `FE.Maybe` — for storing the value of a computation, or explicitly stating 12 | that the computation returned no value (no more `t | nil` returned by 13 | functions). Similar to Elm's 14 | [Maybe](https://package.elm-lang.org/packages/elm/core/latest/Maybe) or 15 | Haskell's 16 | [Data.Maybe](http://hackage.haskell.org/package/base-4.12.0.0/docs/Data-Maybe.html); 17 | * `FE.Result`— for indicating that a computation either successfully output a 18 | value or failed to do so, where the reason for failure can be acted on 19 | further.. Similar to Elm's 20 | [Result](https://package.elm-lang.org/packages/elm-lang/core/latest/Result) or 21 | Haskell's 22 | [Data.Either](http://hackage.haskell.org/package/base-4.12.0.0/docs/Data-Either.html); 23 | * `FE.Review` — for indicating that a computation either succeeded completely, 24 | failed completely, or returned something meaningful, but problems were detected 25 | in the process. Similar to Haskell's 26 | [Data.These](http://hackage.haskell.org/package/these-0.7.5/docs/Data-These.html). 27 | 28 | For more details about each of these types and detailed documentation, please consult 29 | the [documentation page](http://hexdocs.pm/fe) on hexdocs. 30 | 31 | ## Installation 32 | 33 | The library is available on hex.pm. You can use it in your project by adding 34 | it to dependencies: 35 | 36 | 37 | ```elixir 38 | defp deps() do 39 | [ 40 | {:fe, "~> 0.1.5"} 41 | ] 42 | end 43 | ``` 44 | 45 | ## License 46 | 47 | This library is licensed under the [MIT License](LICENSE). 48 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import {} }: 2 | 3 | with pkgs; 4 | 5 | let 6 | erlang = beam.interpreters.erlangR22; 7 | elixir = beam.packages.erlangR22.elixir_1_10; 8 | in 9 | 10 | mkShell { 11 | buildInputs = [ erlang elixir ]; 12 | } 13 | -------------------------------------------------------------------------------- /fe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/well-ironed/fe/264fe475af06039c604dd1a9fe994b2a400a062f/fe.png -------------------------------------------------------------------------------- /lib/fe.ex: -------------------------------------------------------------------------------- 1 | defmodule FE do 2 | @moduledoc """ 3 | Basic functional programming idioms. 4 | """ 5 | 6 | @doc """ 7 | Always return the argument unchanged. 8 | """ 9 | @spec id(a) :: a when a: var 10 | def id(a), do: a 11 | 12 | @doc """ 13 | Create a unary function that always returns the same value, regardless 14 | of what it was called with. 15 | """ 16 | @spec const(a) :: (any -> a) when a: var 17 | def const(a), do: fn _ -> a end 18 | end 19 | -------------------------------------------------------------------------------- /lib/fe/maybe.ex: -------------------------------------------------------------------------------- 1 | defmodule FE.Maybe do 2 | @moduledoc """ 3 | `FE.Maybe` is an explicit data type for representing values that might or might not exist. 4 | """ 5 | 6 | alias FE.{Result, Review} 7 | 8 | @type t(a) :: {:just, a} | :nothing 9 | 10 | defmodule Error do 11 | defexception [:message] 12 | end 13 | 14 | @doc """ 15 | Creates an `FE.Maybe` representing the absence of a value. 16 | """ 17 | @spec nothing() :: t(any) 18 | def nothing, do: :nothing 19 | 20 | @doc """ 21 | Creates an `FE.Maybe` representing the value passed as an argument. 22 | """ 23 | @spec just(a) :: t(a) when a: var 24 | def just(value), do: {:just, value} 25 | 26 | @doc """ 27 | Creates an `FE.Maybe` from any Elixir term. 28 | 29 | It creates a non-value from `nil` and a value from any other term. 30 | Please note that false, 0, the empty list, etc., are valid values. 31 | 32 | ## Examples 33 | iex> FE.Maybe.new(nil) 34 | FE.Maybe.nothing() 35 | 36 | iex> FE.Maybe.new(:x) 37 | FE.Maybe.just(:x) 38 | 39 | iex> FE.Maybe.new(false) 40 | FE.Maybe.just(false) 41 | """ 42 | @spec new(a | nil) :: t(a) when a: var 43 | def new(term) 44 | def new(nil), do: nothing() 45 | def new(value), do: just(value) 46 | 47 | @doc """ 48 | Transforms an `FE.Maybe` value using the provided function. 49 | Nothing is done if there is no value. 50 | 51 | ## Examples 52 | iex> FE.Maybe.map(FE.Maybe.nothing(), &String.length/1) 53 | FE.Maybe.nothing() 54 | 55 | iex> FE.Maybe.map(FE.Maybe.just("foo"), &String.length/1) 56 | FE.Maybe.just(3) 57 | """ 58 | @spec map(t(a), (a -> a)) :: t(a) when a: var 59 | def map(maybe, f) 60 | def map(:nothing, _), do: nothing() 61 | def map({:just, value}, f), do: just(f.(value)) 62 | 63 | @doc """ 64 | Returns the value stored in a `FE.Maybe` or the provided default if there is no value. 65 | 66 | ## Examples 67 | iex> FE.Maybe.unwrap_or(FE.Maybe.nothing(), 0) 68 | 0 69 | 70 | iex> FE.Maybe.unwrap_or(FE.Maybe.just(5), 0) 71 | 5 72 | """ 73 | @spec unwrap_or(t(a), a) :: a when a: var 74 | def unwrap_or(maybe, default) 75 | def unwrap_or(:nothing, default), do: default 76 | def unwrap_or({:just, value}, _), do: value 77 | 78 | @doc """ 79 | Passes the value stored in `FE.Maybe` as input to the first function, or returns the provided default. 80 | 81 | ## Examples 82 | iex> FE.Maybe.unwrap_with(FE.Maybe.nothing(), fn(x) -> x+1 end, 0) 83 | 0 84 | 85 | iex> FE.Maybe.unwrap_with(FE.Maybe.just(4), fn(x) -> x+1 end, 0) 86 | 5 87 | 88 | iex> FE.Maybe.unwrap_with(FE.Maybe.just("a"), fn(x) -> x <> "bc" end, "xyz") 89 | "abc" 90 | """ 91 | @spec unwrap_with(t(a), (a -> b), b) :: b when a: var, b: var 92 | def unwrap_with(maybe, on_just, default) 93 | def unwrap_with(:nothing, _, default), do: default 94 | def unwrap_with({:just, value}, on_just, _), do: on_just.(value) 95 | 96 | @doc """ 97 | Returns the value stored in an `FE.Maybe`. Raises an `FE.Maybe.Error` if a non-value is passed. 98 | 99 | ## Examples 100 | iex> FE.Maybe.unwrap!(FE.Maybe.just(:value)) 101 | :value 102 | 103 | iex> try do FE.Maybe.unwrap!(FE.Maybe.nothing()) ; rescue e -> e end 104 | %FE.Maybe.Error{message: "unwrapping Maybe that has no value"} 105 | """ 106 | @spec unwrap!(t(a)) :: a | no_return() when a: var 107 | def unwrap!(maybe) 108 | def unwrap!({:just, value}), do: value 109 | def unwrap!(:nothing), do: raise(Error, "unwrapping Maybe that has no value") 110 | 111 | @doc """ 112 | Passes the value of `FE.Maybe` to the provided function and returns its return value, 113 | that should be of the type `FE.Maybe`. 114 | 115 | Useful for chaining together a computation consisting of multiple steps, each of which 116 | takes a value as an argument and returns a `FE.Maybe`. 117 | 118 | ## Examples 119 | iex> FE.Maybe.and_then(FE.Maybe.nothing(), fn s -> FE.Maybe.just(String.length(s)) end) 120 | FE.Maybe.nothing() 121 | 122 | iex> FE.Maybe.and_then(FE.Maybe.just("foobar"), fn s -> FE.Maybe.just(String.length(s)) end) 123 | FE.Maybe.just(6) 124 | 125 | iex> FE.Maybe.and_then(FE.Maybe.just("foobar"), fn _ -> FE.Maybe.nothing() end) 126 | FE.Maybe.nothing() 127 | """ 128 | @spec and_then(t(a), (a -> t(a))) :: t(a) when a: var 129 | def and_then(maybe, f) 130 | def and_then(:nothing, _), do: nothing() 131 | def and_then({:just, value}, f), do: f.(value) 132 | 133 | @doc """ 134 | Folds over the provided list of elements, where the accumulator and each element 135 | in the list are passed to the provided function. 136 | 137 | 138 | The provided function must returns a new accumulator of the `FE.Maybe` type. 139 | The provided `FE.Maybe` is the initial accumulator. 140 | 141 | Returns the last `FE.Maybe` returned by the function. 142 | 143 | Stops and returns `nothing()` if at any step the function returns `nothing`. 144 | 145 | ## Examples 146 | iex> FE.Maybe.fold(FE.Maybe.nothing(), [], &FE.Maybe.just(&1)) 147 | FE.Maybe.nothing() 148 | 149 | iex> FE.Maybe.fold(FE.Maybe.just(5), [], &FE.Maybe.just(&1)) 150 | FE.Maybe.just(5) 151 | 152 | iex> FE.Maybe.fold(FE.Maybe.nothing(), [1, 2], &FE.Maybe.just(&1 + &2)) 153 | FE.Maybe.nothing() 154 | 155 | iex> FE.Maybe.fold(FE.Maybe.just(1), [1, 1], &FE.Maybe.just(&1 + &2)) 156 | FE.Maybe.just(3) 157 | 158 | iex> FE.Maybe.fold(FE.Maybe.just(1), [1, 2, -2, 3], fn 159 | ...> elem, _acc when elem < 0 -> FE.Maybe.nothing() 160 | ...> elem, acc -> FE.Maybe.just(elem+acc) 161 | ...> end) 162 | FE.Maybe.nothing() 163 | """ 164 | @spec fold(t(a), [b], (b, a -> t(a))) :: t(a) when a: var, b: var 165 | def fold(maybe, elems, f) do 166 | Enum.reduce_while(elems, maybe, fn elem, acc -> 167 | case and_then(acc, fn value -> f.(elem, value) end) do 168 | {:just, _} = just -> {:cont, just} 169 | :nothing -> {:halt, :nothing} 170 | end 171 | end) 172 | end 173 | 174 | @doc """ 175 | Works like `fold/3`, except that the first element of the provided list is removed 176 | from it, wrapped in a `FE.Maybe` and treated as the initial accumulator. 177 | 178 | Then, fold is executed over the remainder of the provided list. 179 | 180 | ## Examples 181 | iex> FE.Maybe.fold([1,2,3], fn elem, acc -> FE.Maybe.just(elem+acc) end) 182 | FE.Maybe.just(6) 183 | 184 | iex> FE.Maybe.fold([1], fn elem, acc -> FE.Maybe.just(elem+acc) end) 185 | FE.Maybe.just(1) 186 | 187 | iex> FE.Maybe.fold([1], fn _, _ -> FE.Maybe.nothing() end) 188 | FE.Maybe.just(1) 189 | 190 | iex> FE.Maybe.fold([1, 2, 3], &(FE.Maybe.just(&1 + &2))) 191 | FE.Maybe.just(6) 192 | 193 | iex> FE.Maybe.fold([1, -22, 3], fn 194 | ...> elem, _acc when elem < 0 -> FE.Maybe.nothing() 195 | ...> elem, acc -> FE.Maybe.just(elem+acc) 196 | ...> end) 197 | FE.Maybe.nothing() 198 | """ 199 | @spec fold([b], (b, a -> t(a))) :: t(a) when a: var, b: var 200 | def fold(elems, f) 201 | def fold([], _), do: raise(Enum.EmptyError) 202 | def fold([head | tail], f), do: fold(just(head), tail, f) 203 | 204 | @doc """ 205 | Extracts only the values from a list of `Maybe.t()`s 206 | 207 | ## Examples 208 | iex> FE.Maybe.justs([FE.Maybe.just(:good), FE.Maybe.nothing(), FE.Maybe.just(:better)]) 209 | [:good, :better] 210 | """ 211 | @spec justs([t(a)]) :: [a] when a: var 212 | def justs(els) do 213 | Enum.reduce(els, [], fn 214 | {:just, value}, acc -> [value | acc] 215 | :nothing, acc -> acc 216 | end) 217 | |> Enum.reverse() 218 | end 219 | 220 | @doc """ 221 | Transforms an `FE.Maybe` to an `FE.Result`. 222 | 223 | An `FE.Maybe` with a value becomes a successful value of a `FE.Result`. 224 | 225 | A `FE.Maybe` without a value wrapped becomes an erroneous `FE.Result`, where 226 | the second argument is used as the error's value. 227 | 228 | 229 | ## Examples 230 | iex> FE.Maybe.to_result(FE.Maybe.just(3), "No number found") 231 | FE.Result.ok(3) 232 | 233 | iex> FE.Maybe.to_result(FE.Maybe.nothing(), "No number found") 234 | FE.Result.error("No number found") 235 | """ 236 | @spec to_result(t(a), b) :: Result.t(a, b) when a: var, b: var 237 | def to_result(maybe, error) 238 | def to_result({:just, value}, _), do: Result.ok(value) 239 | def to_result(:nothing, error), do: Result.error(error) 240 | 241 | @doc """ 242 | Transforms an `FE.Maybe` to an `FE.Review`. 243 | 244 | An `FE.Maybe` with a value becomes an accepted `FE.Review` with the same value. 245 | 246 | An `FE.Maybe` without a value wrapped becomes a rejected `FE.Review`, where 247 | the issues are takens from the second argument to the function. 248 | 249 | 250 | ## Examples 251 | iex> FE.Maybe.to_review(FE.Maybe.just(3), ["No number found"]) 252 | FE.Review.accepted(3) 253 | 254 | iex> FE.Maybe.to_review(FE.Maybe.nothing(), ["No number found"]) 255 | FE.Review.rejected(["No number found"]) 256 | """ 257 | @spec to_review(t(a), [b]) :: Review.t(a, b) when a: var, b: var 258 | def to_review(maybe, issues) 259 | def to_review({:just, value}, _), do: Review.accepted(value) 260 | def to_review(:nothing, issues), do: Review.rejected(issues) 261 | end 262 | -------------------------------------------------------------------------------- /lib/fe/result.ex: -------------------------------------------------------------------------------- 1 | defmodule FE.Result do 2 | @moduledoc """ 3 | `FE.Result` is a data type for representing output of a computation that either succeeded or failed. 4 | """ 5 | @type t(a, b) :: {:ok, a} | {:error, b} 6 | @type t(a) :: t(a, any) 7 | 8 | alias FE.{Maybe, Review} 9 | 10 | defmodule Error do 11 | defexception [:message] 12 | end 13 | 14 | @doc """ 15 | Creates a `FE.Result` representing a successful output of a computation. 16 | """ 17 | @spec ok(a) :: t(a) when a: var 18 | def ok(value), do: {:ok, value} 19 | 20 | @doc """ 21 | Creates a `FE.Result` representing an errorneous output of a computation. 22 | """ 23 | @spec error(a) :: t(any, a) when a: var 24 | def error(value), do: {:error, value} 25 | 26 | @doc """ 27 | Transforms a success value in a `FE.Result` using a provided function. 28 | 29 | ## Examples 30 | iex> FE.Result.map(FE.Result.error("foo"), &String.length/1) 31 | FE.Result.error("foo") 32 | 33 | iex> FE.Result.map(FE.Result.ok("foo"), &String.length/1) 34 | FE.Result.ok(3) 35 | """ 36 | @spec map(t(a, b), (a -> c)) :: t(c, b) when a: var, b: var, c: var 37 | def map(result, f) 38 | def map({:error, _} = error, _), do: error 39 | def map({:ok, value}, f), do: {:ok, f.(value)} 40 | 41 | @doc """ 42 | Transforms an errorneous value in a `FE.Result` using a provided function. 43 | 44 | ## Examples 45 | iex> FE.Result.map_error(FE.Result.ok("foo"), &String.length/1) 46 | FE.Result.ok("foo") 47 | 48 | iex> FE.Result.map_error(FE.Result.error("foo"), &String.length/1) 49 | FE.Result.error(3) 50 | """ 51 | @spec map_error(t(a, b), (b -> c)) :: t(a, c) when a: var, b: var, c: var 52 | def map_error(result, f) 53 | def map_error({:ok, _} = ok, _), do: ok 54 | def map_error({:error, value}, f), do: {:error, f.(value)} 55 | 56 | @doc """ 57 | Returns the success value stored in a `FE.Result` or a provided default value if an error is passed. 58 | 59 | ## Examples 60 | iex> FE.Result.unwrap_or(FE.Result.error("foo"), "default") 61 | "default" 62 | 63 | iex> FE.Result.unwrap_or(FE.Result.ok("bar"), "default") 64 | "bar" 65 | """ 66 | @spec unwrap_or(t(a), a) :: a when a: var 67 | def unwrap_or(result, default) 68 | def unwrap_or({:error, _}, default), do: default 69 | def unwrap_or({:ok, value}, _), do: value 70 | 71 | @doc """ 72 | Returns the success value stored in a `FE.Result`, raises an `FE.Result.Error` if an error is passed. 73 | """ 74 | @spec unwrap!(t(a)) :: a | no_return() when a: var 75 | def unwrap!(result) 76 | def unwrap!({:ok, value}), do: value 77 | 78 | def unwrap!({:error, error}) do 79 | raise(Error, "unwrapping Result with an error: #{inspect(error)}") 80 | end 81 | 82 | @doc """ 83 | Runs the first function on a success value, or the second function on 84 | error value, returning the results. 85 | 86 | ## Examples 87 | 88 | iex> FE.Result.ok(1) |> FE.Result.unwrap_with(&inspect/1, &("error: "<> inspect(&1))) 89 | "1" 90 | 91 | iex> FE.Result.error("db down") |> FE.Result.unwrap_with(&inspect/1, &("error: "<> &1)) 92 | "error: db down" 93 | 94 | """ 95 | @spec unwrap_with(t(a, b), (a -> c), (b -> d)) :: c | d when a: var, b: var, c: var, d: var 96 | def unwrap_with(result, on_ok, on_error) 97 | def unwrap_with({:ok, value}, f, _) when is_function(f, 1), do: f.(value) 98 | def unwrap_with({:error, error}, _, f) when is_function(f, 1), do: f.(error) 99 | 100 | @doc """ 101 | Applies success value of a `FE.Result` to a provided function and returns its return value, 102 | that should be of `FE.Result` type. 103 | 104 | Useful for chaining together a computation consisting of multiple steps, each of which 105 | takes success value wrapped in `FE.Result` as an argument and returns a `FE.Result`. 106 | 107 | ## Examples 108 | iex> FE.Result.error("foo") |> FE.Result.and_then(&FE.Result.ok(String.length(&1))) 109 | FE.Result.error("foo") 110 | 111 | iex> FE.Result.ok("bar") |> FE.Result.and_then(&FE.Result.ok(String.length(&1))) 112 | FE.Result.ok(3) 113 | 114 | iex> FE.Result.ok("bar") |> FE.Result.and_then(fn _ -> FE.Result.error(:baz) end) 115 | FE.Result.error(:baz) 116 | """ 117 | @spec and_then(t(a, b), (a -> t(c, b))) :: t(c, b) when a: var, b: var, c: var 118 | def and_then(result, f) 119 | def and_then({:error, _} = error, _), do: error 120 | def and_then({:ok, value}, f), do: f.(value) 121 | 122 | @doc """ 123 | Folds over provided list of elements applying it and current accumulator 124 | to the provided function. 125 | 126 | The provided function returns a new accumulator, that should be a `FE.Result`. 127 | The provided `FE.Result` is the initial accumulator. 128 | 129 | Returns last value returned by the function. 130 | 131 | Stops and returns error if at any moment the function returns error. 132 | 133 | ## Examples 134 | iex> FE.Result.fold(FE.Result.error(:error), [], &FE.Result.ok(&1 + &2)) 135 | FE.Result.error(:error) 136 | 137 | iex> FE.Result.fold(FE.Result.ok(5), [], &FE.Result.ok(&1 + &2)) 138 | FE.Result.ok(5) 139 | 140 | iex> FE.Result.fold(FE.Result.error(:foo), [1, 2], &FE.Result.ok(&1 + &2)) 141 | FE.Result.error(:foo) 142 | 143 | iex> FE.Result.fold(FE.Result.ok(5), [1, 2, 3], &FE.Result.ok(&1 * &2)) 144 | FE.Result.ok(30) 145 | 146 | iex> FE.Result.fold(FE.Result.ok(5), [1, 2, 3], fn 147 | ...> _, 10 -> FE.Result.error("it's a ten!") 148 | ...> x, y -> FE.Result.ok(x * y) 149 | ...> end) 150 | FE.Result.error("it's a ten!") 151 | """ 152 | @spec fold(t(a, b), [c], (c, a -> t(a, b))) :: t(a, b) when a: var, b: var, c: var 153 | def fold(result, elems, f) do 154 | Enum.reduce_while(elems, result, fn elem, acc -> 155 | case and_then(acc, fn value -> f.(elem, value) end) do 156 | {:ok, _} = ok -> {:cont, ok} 157 | {:error, _} = error -> {:halt, error} 158 | end 159 | end) 160 | end 161 | 162 | @doc """ 163 | Works like `fold/3`, except that the first element of the provided list is removed 164 | from it, converted to a success `FE.Result` and treated as the initial accumulator. 165 | 166 | Then, fold is executed over the remainder of the provided list. 167 | 168 | ## Examples 169 | iex> FE.Result.fold([1], fn _, _ -> FE.Result.error(:one) end) 170 | FE.Result.ok(1) 171 | 172 | iex> FE.Result.fold([1, 2, 3], &(FE.Result.ok(&1 + &2))) 173 | FE.Result.ok(6) 174 | 175 | iex> FE.Result.fold([1, 2, 3], fn 176 | ...> _, 3 -> FE.Result.error(:three) 177 | ...> x, y -> FE.Result.ok(x + y) 178 | ...> end) 179 | FE.Result.error(:three) 180 | """ 181 | @spec fold([c], (c, a -> t(a, b))) :: t(a, b) when a: var, b: var, c: var 182 | def fold(elems, f) 183 | def fold([], _), do: raise(Enum.EmptyError) 184 | def fold([head | tail], f), do: fold(ok(head), tail, f) 185 | 186 | @doc """ 187 | Returns the `FE.Result.ok` values from a list of `FE.Result`s. 188 | 189 | ## Examples 190 | iex> FE.Result.oks([FE.Result.ok(:good), FE.Result.error(:bad), FE.Result.ok(:better)]) 191 | [:good, :better] 192 | """ 193 | 194 | @spec oks([t(a, any)]) :: [a] when a: var 195 | def oks(e) do 196 | Enum.reduce(e, [], fn 197 | {:ok, val}, acc -> [val | acc] 198 | {:error, _}, acc -> acc 199 | end) 200 | |> Enum.reverse() 201 | end 202 | 203 | @doc """ 204 | If a list of `FE.Result`s is all `FE.Result.ok`s, returns a `FE.Result.ok` 205 | where the value is a list of the unwrapped values. 206 | 207 | Otherwise, returns `FE.Result.error` with the first erroneous value. 208 | 209 | ## Examples 210 | iex> FE.Result.all_ok([FE.Result.ok(:a), FE.Result.ok(:b), FE.Result.ok(:c)]) 211 | FE.Result.ok([:a, :b, :c]) 212 | iex> FE.Result.all_ok([FE.Result.ok(:a), FE.Result.error("BAD APPLE"), FE.Result.ok(:c)]) 213 | FE.Result.error("BAD APPLE") 214 | """ 215 | 216 | @spec all_ok([t(a, any)]) :: t([a], any) when a: var 217 | def all_ok(list), do: all_ok0(list, []) 218 | 219 | defp all_ok0([], res) when is_list(res), do: Enum.reverse(res) |> ok() 220 | defp all_ok0([{:ok, v} | tail], res), do: all_ok0(tail, [v | res]) 221 | defp all_ok0([{:error, e} | _], _), do: {:error, e} 222 | 223 | @doc """ 224 | Transforms `FE.Result` to a `FE.Maybe`. 225 | 226 | A `FE.Result` with successful value becomes a `FE.Maybe` with the same value. 227 | 228 | An errornous `FE.Result` becomes a `FE.Maybe` without a value. 229 | 230 | ## Examples 231 | iex> FE.Result.to_maybe(FE.Result.ok(13)) 232 | FE.Maybe.just(13) 233 | 234 | iex> FE.Result.to_maybe(FE.Result.error("something went wrong")) 235 | FE.Maybe.nothing() 236 | """ 237 | @spec to_maybe(t(a, any)) :: Maybe.t(a) when a: var 238 | def to_maybe(result) 239 | def to_maybe({:ok, value}), do: Maybe.just(value) 240 | def to_maybe({:error, _}), do: Maybe.nothing() 241 | 242 | @doc """ 243 | Transforms `FE.Result` to a `FE.Review`. 244 | 245 | A `FE.Result` with successful value becomes an accepted `FE.Review` with 246 | the same value. 247 | 248 | An errornous `FE.Result` with error output being a list becomes a rejected 249 | `FE.Review` with issues being exactly this list. 250 | 251 | An errornous `FE.Result` with error output being other term becomes a rejected 252 | `FE.Review` with one issue, being this term. 253 | 254 | ## Examples 255 | iex> FE.Result.to_review(FE.Result.ok(23)) 256 | FE.Review.accepted(23) 257 | 258 | iex> FE.Result.to_review(FE.Result.error(["wrong", "bad", "very bad"])) 259 | FE.Review.rejected(["wrong", "bad", "very bad"]) 260 | 261 | iex> FE.Result.to_review(FE.Result.error("error")) 262 | FE.Review.rejected(["error"]) 263 | """ 264 | @spec to_review(t(a, b) | t(a, [b])) :: Review.t(a, b) when a: var, b: var 265 | def to_review(result) 266 | def to_review({:ok, value}), do: Review.accepted(value) 267 | def to_review({:error, values}) when is_list(values), do: Review.rejected(values) 268 | def to_review({:error, value}), do: Review.rejected([value]) 269 | end 270 | -------------------------------------------------------------------------------- /lib/fe/review.ex: -------------------------------------------------------------------------------- 1 | defmodule FE.Review do 2 | @moduledoc """ 3 | `FE.Review` is a data type similar to `FE.Result`, made for representing 4 | output of a computation that either succeed (`accepted`) or fail (`rejected`), 5 | but that might continue despite of issues encountered (`issues`). 6 | 7 | One could say that the type is a specific implementation of a writer monad, 8 | that collects issues encountered during some computation. 9 | 10 | For instance, it might be used for validation of a user input, when we don't 11 | want to stop the process of validation when we encounter the first mistake, 12 | but rather we would like to collect all the user's mistakes before returning 13 | feedback to her. 14 | """ 15 | 16 | @type t(a, b) :: {:accepted, a} | {:issues, a, [b]} | {:rejected, [b]} 17 | 18 | alias FE.{Maybe, Result} 19 | 20 | defmodule Error do 21 | defexception [:message] 22 | end 23 | 24 | @doc """ 25 | Creates a `FE.Review` representing a successful output of a computation. 26 | """ 27 | @spec accepted(a) :: t(a, any) when a: var 28 | def accepted(value), do: {:accepted, value} 29 | 30 | @doc """ 31 | Creates a `FE.Review` representing an errornous output of a computation with 32 | a list of issues encountered during the computation. 33 | """ 34 | @spec rejected([b]) :: t(any, b) when b: var 35 | def rejected(issues) when is_list(issues), do: {:rejected, issues} 36 | 37 | @doc """ 38 | Creates a `FE.Review` representing a problematic output of a computation 39 | that there were some issues with. 40 | """ 41 | @spec issues(a, b) :: t(a, b) when a: var, b: var 42 | def issues(value, issues) when is_list(issues), do: {:issues, value, issues} 43 | 44 | @doc """ 45 | Transforms a successful or a problematic value in a `FE.Review` using 46 | a provided function. 47 | 48 | ## Examples 49 | iex> FE.Review.map(FE.Review.rejected(["foo"]), &String.length/1) 50 | FE.Review.rejected(["foo"]) 51 | 52 | iex> FE.Review.map(FE.Review.issues("foo", ["b", "ar"]), &String.length/1) 53 | FE.Review.issues(3, ["b", "ar"]) 54 | 55 | iex> FE.Review.map(FE.Review.accepted("baz"), &String.length/1) 56 | FE.Review.accepted(3) 57 | """ 58 | @spec map(t(a, b), (a -> c)) :: t(c, b) when a: var, b: var, c: var 59 | def map(review, f) 60 | def map({:accepted, value}, f), do: accepted(f.(value)) 61 | def map({:issues, value, issues}, f), do: issues(f.(value), issues) 62 | def map({:rejected, issues}, _), do: rejected(issues) 63 | 64 | @doc """ 65 | Transform issues stored in a `FE.Review` using a provided function. 66 | 67 | ## Examples 68 | iex> FE.Review.map_issues(FE.Review.accepted("ack!"), &String.length/1) 69 | FE.Review.accepted("ack!") 70 | 71 | iex> FE.Review.map_issues(FE.Review.issues("a", ["bb", "ccc"]), &String.length/1) 72 | FE.Review.issues("a", [2, 3]) 73 | 74 | iex> FE.Review.map_issues(FE.Review.rejected(["dddd", "eeeee"]), &String.length/1) 75 | FE.Review.rejected([4, 5]) 76 | """ 77 | @spec map_issues(t(a, b), (b -> c)) :: t(a, c) when a: var, b: var, c: var 78 | def map_issues(review, f) 79 | def map_issues({:accepted, value}, _), do: accepted(value) 80 | 81 | def map_issues({:issues, value, issues}, f) do 82 | issues(value, Enum.map(issues, f)) 83 | end 84 | 85 | def map_issues({:rejected, issues}, f) do 86 | rejected(Enum.map(issues, f)) 87 | end 88 | 89 | @doc """ 90 | Returns the accepted value stored in a `FE.Review` or a provided default if 91 | either rejected or value with issues is passed 92 | 93 | ## Examples 94 | iex> FE.Review.unwrap_or(FE.Review.rejected(["no", "way"]), :default) 95 | :default 96 | 97 | iex> FE.Review.unwrap_or(FE.Review.issues(1, ["no", "way"]), :default) 98 | :default 99 | 100 | iex> FE.Review.unwrap_or(FE.Review.accepted(123), :default) 101 | 123 102 | """ 103 | @spec unwrap_or(t(a, any), a) :: a when a: var 104 | def unwrap_or(review, default) 105 | def unwrap_or({:rejected, _}, default), do: default 106 | def unwrap_or({:issues, _, _}, default), do: default 107 | def unwrap_or({:accepted, value}, _), do: value 108 | 109 | @doc """ 110 | Returns the accepted value stored in a `FE.Review`, raises an `FE.Review.Error` 111 | if either rejected or value with issues is passed 112 | 113 | ## Examples 114 | iex> FE.Review.unwrap!(FE.Review.accepted("foo")) 115 | "foo" 116 | """ 117 | @spec unwrap!(t(a, any)) :: a when a: var 118 | def unwrap!(review) 119 | def unwrap!({:accepted, value}), do: value 120 | 121 | def unwrap!({:rejected, issues}) do 122 | raise(Error, "unwrapping rejected Review with issues: #{inspect(issues)}") 123 | end 124 | 125 | def unwrap!({:issues, _, issues}) do 126 | raise(Error, "unwrapping Review with issues: #{inspect(issues)}") 127 | end 128 | 129 | @doc """ 130 | Applies accepted value of a `FE.Review` to a provided function. 131 | Returns its return value, that should be of `FE.Review` type. 132 | 133 | Applies value with issues of a `FE.Review` to a provided function. 134 | If accepted value is returned, then the value is replaced, but the issues 135 | remain the same. 136 | If new value with issues is returned, then the value is replaced and the issues 137 | are appended to the list of current issues. 138 | If rejected is returned, then the issues are appended to the list of current issues, 139 | if issues were passed. 140 | 141 | Useful for chaining together a computation consisting of multiple steps, 142 | each of which takes either a success value or value with issues wrapped in 143 | `FE.Review` as an argument and returns a `FE.Review`. 144 | 145 | ## Examples 146 | iex> FE.Review.and_then( 147 | ...> FE.Review.rejected(["foo"]), 148 | ...> &FE.Review.accepted(String.length(&1))) 149 | FE.Review.rejected(["foo"]) 150 | 151 | iex> FE.Review.and_then( 152 | ...> FE.Review.issues("foo", ["bar", "baz"]), 153 | ...> &FE.Review.accepted(String.length(&1))) 154 | FE.Review.issues(3, ["bar", "baz"]) 155 | 156 | iex> FE.Review.and_then( 157 | ...> FE.Review.issues("foo", ["bar", "baz"]), 158 | ...> &FE.Review.issues(String.length(&1), ["qux"])) 159 | FE.Review.issues(3, ["bar", "baz", "qux"]) 160 | 161 | iex> FE.Review.and_then(FE.Review.accepted(1), &FE.Review.issues(&1, [:one])) 162 | FE.Review.issues(1, [:one]) 163 | """ 164 | @spec and_then(t(a, b), (a -> t(c, b))) :: t(c, b) when a: var, b: var, c: var 165 | def and_then(review, f) 166 | 167 | def and_then({:accepted, value}, f) do 168 | case f.(value) do 169 | {:accepted, value} -> accepted(value) 170 | {:issues, value, issues} -> issues(value, issues) 171 | {:rejected, issues} -> rejected(issues) 172 | end 173 | end 174 | 175 | def and_then({:issues, value, issues}, f) do 176 | case f.(value) do 177 | {:accepted, value} -> issues(value, issues) 178 | {:issues, value, new_issues} -> issues(value, issues ++ new_issues) 179 | {:rejected, new_issues} -> rejected(issues ++ new_issues) 180 | end 181 | end 182 | 183 | def and_then({:rejected, value}, _), do: {:rejected, value} 184 | 185 | @doc """ 186 | Folds over provided list of elements applying it and current accumulator 187 | to the provided function. 188 | 189 | The next accumulator is the same as the result of calling `and_then` with the 190 | current accumulator and the provided function. 191 | 192 | The provided `FE.Review` is initial accumulator. 193 | 194 | ## Examples 195 | iex> FE.Review.fold(FE.Review.rejected([:error]), [], 196 | ...> &FE.Review.accepted(&1 + &2)) 197 | FE.Review.rejected([:error]) 198 | 199 | iex> FE.Review.fold(FE.Review.accepted(5), [1, 2, 3], 200 | ...> &FE.Review.accepted(&1 * &2)) 201 | FE.Review.accepted(30) 202 | 203 | iex> FE.Review.fold(FE.Review.accepted(5), [1, 2, 3], 204 | ...> &FE.Review.issues(&1 * &2, [&1])) 205 | FE.Review.issues(30, [1, 2, 3]) 206 | 207 | iex> FE.Review.fold(FE.Review.issues(5, [:five]), [1, 2, 3], 208 | ...> &FE.Review.accepted(&1 * &2)) 209 | FE.Review.issues(30, [:five]) 210 | 211 | iex> FE.Review.fold(FE.Review.accepted(5), [1, 2, 3], fn 212 | ...> x, 10 -> FE.Review.issues(x * 10, ["it's a ten!"]) 213 | ...> x, y -> FE.Review.accepted(x * y) 214 | ...> end) 215 | FE.Review.issues(30, ["it's a ten!"]) 216 | 217 | iex> FE.Review.fold(FE.Review.accepted(5), [1, 2, 3], fn 218 | ...> _, 10 -> FE.Review.rejected(["it's a ten!"]) 219 | ...> x, y -> FE.Review.accepted(x * y) 220 | ...> end) 221 | FE.Review.rejected(["it's a ten!"]) 222 | """ 223 | @spec fold(t(a, b), [c], (c, a -> t(a, b))) :: t(a, b) when a: var, b: var, c: var 224 | def fold(review, elems, f) do 225 | Enum.reduce_while(elems, review, fn elem, acc -> 226 | case and_then(acc, fn value -> f.(elem, value) end) do 227 | {:accepted, _} = accepted -> {:cont, accepted} 228 | {:issues, _, _} = issues -> {:cont, issues} 229 | {:rejected, _} = rejected -> {:halt, rejected} 230 | end 231 | end) 232 | end 233 | 234 | @doc """ 235 | Works like `fold/3`, except that the first element of the provided list is removed 236 | from it, converted to an accepted `FE.Review` and treated as the initial accumulator. 237 | 238 | Then, fold is executed over the remainder of the provided list. 239 | 240 | ## Examples 241 | iex> FE.Review.fold([1], fn _, _ -> FE.Review.rejected([:foo]) end) 242 | FE.Review.accepted(1) 243 | 244 | iex> FE.Review.fold([1, 2, 3], &FE.Review.accepted(&1 + &2)) 245 | FE.Review.accepted(6) 246 | 247 | iex> FE.Review.fold([1, 2, 3], &FE.Review.issues(&1 + &2, [&2])) 248 | FE.Review.issues(6, [1, 3]) 249 | 250 | iex> FE.Review.fold([1, 2, 3, 4], fn 251 | ...> _, 6 -> FE.Review.rejected(["six"]) 252 | ...> x, y -> FE.Review.issues(x + y, [y]) 253 | ...> end) 254 | FE.Review.rejected([1, 3, "six"]) 255 | 256 | iex> FE.Review.fold([1, 2, 3, 4], fn 257 | ...> x, 6 -> FE.Review.issues(x + 6, ["six"]) 258 | ...> x, y -> FE.Review.accepted(x + y) 259 | ...> end) 260 | FE.Review.issues(10, ["six"]) 261 | """ 262 | @spec fold([c], (c, a -> t(a, b))) :: t(a, b) when a: var, b: var, c: var 263 | def fold([], _), do: raise(Enum.EmptyError) 264 | def fold([head | tail], f), do: fold(accepted(head), tail, f) 265 | 266 | @doc """ 267 | Transforms `FE.Review` to a `FE.Result`. 268 | 269 | Any accepted value of a `FE.Review` becomes a successful value of a `FE.Result`. 270 | 271 | If there are any issues either in a rejected `FE.Review` or coupled with a value, 272 | all the issues become a errornous output of the output `FE.Result`. 273 | 274 | ## Examples 275 | iex> FE.Review.to_result(FE.Review.issues(1, [2, 3])) 276 | FE.Result.error([2, 3]) 277 | 278 | iex> FE.Review.to_result(FE.Review.accepted(4)) 279 | FE.Result.ok(4) 280 | 281 | iex> FE.Review.to_result(FE.Review.rejected([5, 6, 7])) 282 | FE.Result.error([5, 6, 7]) 283 | """ 284 | @spec to_result(t(a, b)) :: Result.t(a, [b]) when a: var, b: var 285 | def to_result(review) 286 | def to_result({:accepted, value}), do: Result.ok(value) 287 | def to_result({:rejected, issues}), do: Result.error(issues) 288 | def to_result({:issues, _, issues}), do: Result.error(issues) 289 | 290 | @doc """ 291 | Transforms `FE.Review` to a `FE.Maybe`. 292 | 293 | Any accepted value of a `FE.Review` becomes a `FE.Maybe` with the same value. 294 | 295 | If there are any issues either in a rejected `FE.Review` or coupled with a value, 296 | a `FE.Maybe` without a value is returned. 297 | 298 | ## Examples 299 | iex> FE.Review.to_maybe(FE.Review.issues(1, [2, 3])) 300 | FE.Maybe.nothing() 301 | 302 | iex> FE.Review.to_maybe(FE.Review.accepted(4)) 303 | FE.Maybe.just(4) 304 | 305 | iex> FE.Review.to_maybe(FE.Review.rejected([5, 6, 7])) 306 | FE.Maybe.nothing() 307 | """ 308 | @spec to_maybe(t(a, any)) :: Maybe.t(a) when a: var 309 | def to_maybe(review) 310 | def to_maybe({:accepted, value}), do: Maybe.just(value) 311 | def to_maybe({:rejected, _}), do: Maybe.nothing() 312 | def to_maybe({:issues, _, _}), do: Maybe.nothing() 313 | end 314 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule FE.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :fe, 7 | deps: deps(), 8 | description: description(), 9 | docs: docs(), 10 | elixir: "~> 1.7", 11 | package: package(), 12 | preferred_cli_env: [dialyzer: :test], 13 | source_url: "https://github.com/well-ironed/fe", 14 | start_permanent: Mix.env() == :prod, 15 | version: "0.1.5" 16 | ] 17 | end 18 | 19 | def application do 20 | [ 21 | extra_applications: [:logger] 22 | ] 23 | end 24 | 25 | defp docs do 26 | [ 27 | main: "readme", 28 | extras: ["README.md"] 29 | ] 30 | end 31 | 32 | defp description do 33 | "Collection of useful data types brought to Elixir from other functional languages." 34 | end 35 | 36 | defp package do 37 | [ 38 | licenses: ["MIT"], 39 | links: %{"GitHub" => "https://github.com/well-ironed/fe"} 40 | ] 41 | end 42 | 43 | defp deps do 44 | [ 45 | {:dialyxir, "~> 0.5.1", only: :test, runtime: false}, 46 | {:ex_doc, "~> 0.19", only: :dev, runtime: false} 47 | ] 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "dialyxir": {:hex, :dialyxir, "0.5.1", "b331b091720fd93e878137add264bac4f644e1ddae07a70bf7062c7862c4b952", [:mix], [], "hexpm"}, 3 | "earmark": {:hex, :earmark, "1.2.6", "b6da42b3831458d3ecc57314dff3051b080b9b2be88c2e5aa41cd642a5b044ed", [:mix], [], "hexpm"}, 4 | "ex_doc": {:hex, :ex_doc, "0.19.1", "519bb9c19526ca51d326c060cb1778d4a9056b190086a8c6c115828eaccea6cf", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.7", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"}, 5 | "makeup": {:hex, :makeup, "0.5.5", "9e08dfc45280c5684d771ad58159f718a7b5788596099bdfb0284597d368a882", [:mix], [{:nimble_parsec, "~> 0.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"}, 6 | "makeup_elixir": {:hex, :makeup_elixir, "0.10.0", "0f09c2ddf352887a956d84f8f7e702111122ca32fbbc84c2f0569b8b65cbf7fa", [:mix], [{:makeup, "~> 0.5.5", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"}, 7 | "nimble_parsec": {:hex, :nimble_parsec, "0.4.0", "ee261bb53214943679422be70f1658fff573c5d0b0a1ecd0f18738944f818efe", [:mix], [], "hexpm"}, 8 | } 9 | -------------------------------------------------------------------------------- /test/fe_test.exs: -------------------------------------------------------------------------------- 1 | defmodule FETest do 2 | use ExUnit.Case, async: true 3 | 4 | test "FE.id/1 always returns its argument" do 5 | some_binary = :crypto.strong_rand_bytes(100) 6 | a_number = System.unique_integer() 7 | a_tuple = :erlang.now() 8 | 9 | assert FE.id(some_binary) == some_binary 10 | assert FE.id(a_number) == a_number 11 | assert FE.id(a_tuple) == a_tuple 12 | end 13 | 14 | test "FE.const(x) creates a function that always returns x" do 15 | some_binary = :crypto.strong_rand_bytes(100) 16 | a_number = System.unique_integer() 17 | a_tuple = :erlang.now() 18 | 19 | assert FE.const(some_binary).(a_number) == some_binary 20 | assert FE.const(a_number).(a_tuple) == a_number 21 | assert FE.const(a_tuple).(some_binary) == a_tuple 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /test/maybe_test.exs: -------------------------------------------------------------------------------- 1 | defmodule FE.MaybeTest do 2 | use ExUnit.Case, async: true 3 | doctest FE.Maybe 4 | 5 | alias FE.{Maybe, Result, Review} 6 | 7 | test "nothing can be created with a constructor" do 8 | assert Maybe.nothing() == :nothing 9 | end 10 | 11 | test "just value can be created with a constructor" do 12 | assert Maybe.just(5) == {:just, 5} 13 | end 14 | 15 | test "nothing is created from nil" do 16 | assert Maybe.new(nil) == Maybe.nothing() 17 | end 18 | 19 | test "just is created from any other value" do 20 | assert Maybe.new(3) == Maybe.just(3) 21 | assert Maybe.new("foo") == Maybe.just("foo") 22 | end 23 | 24 | test "map doesn't apply function to nothing" do 25 | assert Maybe.map(Maybe.nothing(), &(&1 + 1)) == Maybe.nothing() 26 | end 27 | 28 | test "map applies function to just value" do 29 | assert Maybe.map(Maybe.just(5), &(&1 * 2)) == Maybe.just(10) 30 | assert Maybe.map(Maybe.just("bar"), &String.length/1) == Maybe.just(3) 31 | end 32 | 33 | test "unwrap_with applies the first function to just value" do 34 | assert Maybe.unwrap_with(Maybe.just(5), &(&1 * 2), :whatever) == 10 35 | end 36 | 37 | test "unwrap_with uses default value for nothing" do 38 | assert Maybe.unwrap_with(Maybe.nothing(), &(&1 * 2), :whatever) == :whatever 39 | end 40 | 41 | test "unwrap_or returns default value if nothing is passed" do 42 | assert Maybe.unwrap_or(Maybe.nothing(), :default) == :default 43 | end 44 | 45 | test "unwrap_or returns just value if just is passed" do 46 | assert Maybe.unwrap_or(Maybe.just(5), nil) == 5 47 | assert Maybe.unwrap_or(Maybe.just("five"), :ok) == "five" 48 | end 49 | 50 | test "unwrap! returns just value is just is passed" do 51 | assert Maybe.unwrap!(Maybe.just(3)) == 3 52 | assert Maybe.unwrap!(Maybe.just("three")) == "three" 53 | end 54 | 55 | test "unwrap! raises an exception if nothing is passed" do 56 | assert_raise FE.Maybe.Error, "unwrapping Maybe that has no value", fn -> 57 | Maybe.unwrap!(Maybe.nothing()) 58 | end 59 | end 60 | 61 | test "and_then returns nothing if nothing is passed" do 62 | assert Maybe.and_then(Maybe.nothing(), fn _ -> Maybe.nothing() end) == Maybe.nothing() 63 | end 64 | 65 | test "and_then applies function if just is passed" do 66 | assert Maybe.and_then(Maybe.just(5), fn x -> Maybe.just(x + 10) end) == Maybe.just(15) 67 | 68 | assert Maybe.and_then(Maybe.just("5"), fn _ -> Maybe.nothing() end) == Maybe.nothing() 69 | end 70 | 71 | test "and_then chain stops on first nothing" do 72 | result = 73 | Maybe.just(1) 74 | |> Maybe.and_then(&Maybe.just(&1 + 2)) 75 | |> Maybe.and_then(fn _ -> Maybe.nothing() end) 76 | |> Maybe.and_then(&Maybe.just(&1 - 4)) 77 | 78 | assert result == Maybe.nothing() 79 | end 80 | 81 | test "and_then chain returns last if there is no nothing on the way" do 82 | result = 83 | Maybe.just(1) 84 | |> Maybe.and_then(&Maybe.just(&1 + 2)) 85 | |> Maybe.and_then(&Maybe.just(&1 * 3)) 86 | |> Maybe.and_then(&Maybe.just(&1 - 4)) 87 | 88 | assert result == Maybe.just(5) 89 | end 90 | 91 | test "fold/3 over an empty list returns passed maybe" do 92 | assert Maybe.fold(Maybe.nothing(), [], &Maybe.just(&1 + &2)) == Maybe.nothing() 93 | 94 | assert Maybe.fold(Maybe.just(5), [], &Maybe.just(&1 + &2)) == Maybe.just(5) 95 | end 96 | 97 | test "fold/3 over a single value applies function to it if the just value passed" do 98 | assert Maybe.fold(Maybe.just(10), [5], &Maybe.just(&1 + &2)) == Maybe.just(15) 99 | 100 | assert Maybe.fold(Maybe.just(20), [3], fn _, _ -> Maybe.nothing() end) == Maybe.nothing() 101 | end 102 | 103 | test "fold/3 over a single value doesn't apply function if nothing is passed" do 104 | assert Maybe.fold(Maybe.nothing(), [5], &Maybe.just(&1 + &2)) == Maybe.nothing() 105 | end 106 | 107 | test "fold/3 over values returns last value returned by function if it returns only justs" do 108 | assert Maybe.fold(Maybe.just(1), [2, 3, 4], &Maybe.just(&1 * &2)) == Maybe.just(24) 109 | end 110 | 111 | test "fold/3 over values returns nothing when the function returns it" do 112 | assert Maybe.fold(Maybe.just(1), [2, 3, 4], fn 113 | _, 6 -> Maybe.nothing() 114 | x, y -> Maybe.just(x + y) 115 | end) == Maybe.nothing() 116 | end 117 | 118 | test "fold/2 over an empty list raises an EmptyError" do 119 | assert_raise Enum.EmptyError, fn -> Maybe.fold([], &Maybe.just(&1 + &2)) end 120 | end 121 | 122 | test "fold/2 over a single value returns this value as just value" do 123 | assert Maybe.fold([1], fn _, _ -> Maybe.nothing() end) == Maybe.just(1) 124 | end 125 | 126 | test "fold/2 over values returns last value returned by function if it returns only justs" do 127 | assert Maybe.fold([5, 6, 7], &Maybe.just(&1 * &2)) == Maybe.just(210) 128 | end 129 | 130 | test "fold/2 over values returns nothing when the function returns it" do 131 | assert Maybe.fold([5, 6, 7], fn 132 | _, 11 -> Maybe.nothing() 133 | x, y -> Maybe.just(x + y) 134 | end) == Maybe.nothing() 135 | end 136 | 137 | test "justs/1 does nothing on empty list" do 138 | assert Maybe.justs([]) == [] 139 | end 140 | 141 | test "justs/1 preserves ordering" do 142 | assert Maybe.justs([Maybe.just(1), Maybe.just(2)]) == [1, 2] 143 | end 144 | 145 | test "justs/1 doesn't return nothings" do 146 | assert Maybe.justs([Maybe.nothing(), Maybe.nothing()]) == [] 147 | end 148 | 149 | test "justs/1 filters out only justs" do 150 | assert Maybe.justs([Maybe.nothing(), Maybe.just(2)]) == [2] 151 | end 152 | 153 | test "to_result converts just value to ok with the same value" do 154 | just = Maybe.just(123) 155 | assert Maybe.to_result(just, "error") == Result.ok(123) 156 | end 157 | 158 | test "to_result converts nothing to error with passed erroreneous value" do 159 | assert Maybe.to_result(Maybe.nothing(), "it's an error") == Result.error("it's an error") 160 | end 161 | 162 | test "to_review converts just value to accepted review with the same value" do 163 | just = Maybe.just(456) 164 | assert Maybe.to_review(just, ["issue"]) == Review.accepted(456) 165 | end 166 | 167 | test "to_review converts nothing to rejected review with passed issues" do 168 | issues = ["issue 1", "issue 2", "issue 3"] 169 | assert Maybe.to_review(Maybe.nothing(), issues) == Review.rejected(issues) 170 | end 171 | end 172 | -------------------------------------------------------------------------------- /test/result_test.exs: -------------------------------------------------------------------------------- 1 | defmodule FE.ResultTest do 2 | use ExUnit.Case, async: true 3 | doctest FE.Result 4 | 5 | alias FE.{Result, Maybe, Review} 6 | 7 | test "ok can be created with a constructor" do 8 | assert Result.ok(:foo) == {:ok, :foo} 9 | end 10 | 11 | test "error can be created with a constructor" do 12 | assert Result.error("bar") == {:error, "bar"} 13 | end 14 | 15 | test "mapping over an error returns the same error" do 16 | assert Result.map(Result.error(:foo), fn _ -> :bar end) == Result.error(:foo) 17 | end 18 | 19 | test "mapping over an ok value applies function to value" do 20 | assert Result.map(Result.ok(2), &(&1 * 5)) == Result.ok(10) 21 | end 22 | 23 | test "map_error over an ok value returns the same result" do 24 | assert Result.map_error(Result.ok(3), fn _ -> :baz end) == Result.ok(3) 25 | end 26 | 27 | test "map_error over an error applies function to the error value" do 28 | assert Result.map_error(Result.error(2), &(&1 * 3)) == Result.error(6) 29 | end 30 | 31 | test "unwrap_or returns default value if an error is passed" do 32 | assert Result.unwrap_or(Result.error(:foo), :default) == :default 33 | assert Result.unwrap_or(Result.error("bar"), nil) == nil 34 | end 35 | 36 | test "unwrap_or returns wrapped value if an ok is passed" do 37 | assert Result.unwrap_or(Result.ok(:bar), :default) == :bar 38 | assert Result.unwrap_or(Result.ok(3), :x) == 3 39 | end 40 | 41 | test "unwrap! returns wrapped value if an ok is passed" do 42 | assert Result.unwrap!(Result.ok(:foo)) == :foo 43 | end 44 | 45 | test "unwrap! raises an exception with error content in message if an error is passed" do 46 | assert_raise Result.Error, "unwrapping Result with an error: :bar", fn -> 47 | Result.unwrap!(Result.error(:bar)) 48 | end 49 | end 50 | 51 | test "unwrap_with runs first function on ok value" do 52 | assert Result.unwrap_with({:ok, "hello"}, &String.length/1, &String.to_atom/1) == 5 53 | end 54 | 55 | test "unwrap_with runs second function on error value" do 56 | assert Result.unwrap_with({:error, "hello"}, &String.length/1, &String.to_atom/1) == :hello 57 | end 58 | 59 | test "and_then returns error if an error is passed" do 60 | assert Result.and_then(Result.error(5), fn x -> Result.ok(x * 2) end) == Result.error(5) 61 | end 62 | 63 | test "and_then applies function to the ok value that's passed" do 64 | assert Result.and_then(Result.ok(5), fn x -> Result.ok(x * 2) end) == Result.ok(10) 65 | end 66 | 67 | test "and_then chain stops on first error" do 68 | result = 69 | Result.ok(1) 70 | |> Result.and_then(&Result.ok(&1 + 2)) 71 | |> Result.and_then(&Result.error(&1 * 3)) 72 | |> Result.and_then(&Result.ok(&1 - 4)) 73 | 74 | assert result == Result.error(9) 75 | end 76 | 77 | test "and_then chain returns last if there is no error on the way" do 78 | result = 79 | Result.ok(1) 80 | |> Result.and_then(&Result.ok(&1 + 2)) 81 | |> Result.and_then(&Result.ok(&1 * 3)) 82 | |> Result.and_then(&Result.ok(&1 - 4)) 83 | 84 | assert result == Result.ok(5) 85 | end 86 | 87 | test "fold/3 over an empty list returns passed result" do 88 | baz = fn _, _ -> Result.ok(:baz) end 89 | assert Result.fold(Result.ok(:foo), [], baz) == Result.ok(:foo) 90 | assert Result.fold(Result.error(:bar), [], baz) == Result.error(:bar) 91 | end 92 | 93 | test "fold/3 over a single value applies function to it if the ok value passed" do 94 | assert Result.fold(Result.ok(10), [5], &Result.ok(&1 + &2)) == Result.ok(15) 95 | 96 | assert Result.fold(Result.ok(20), [5], fn _, _ -> Result.error(:bar) end) == 97 | Result.error(:bar) 98 | end 99 | 100 | test "fold/3 over a single value doesn't apply function if error is passed" do 101 | assert Result.fold(Result.error(:foo), [5], &Result.ok(&1 + &2)) == Result.error(:foo) 102 | end 103 | 104 | test "fold/3 over values returns last value returned by function if it returns only oks" do 105 | assert Result.fold(Result.ok(1), [2, 3, 4], &Result.ok(&1 * &2)) == Result.ok(24) 106 | end 107 | 108 | test "fold/3 over values returns error when the function returns it" do 109 | assert Result.fold(Result.ok(1), [2, 3, 4], fn 110 | _, 6 -> Result.error("it's a six!") 111 | x, y -> Result.ok(x + y) 112 | end) == Result.error("it's a six!") 113 | end 114 | 115 | test "fold/2 over an empty list raises an EmptyError" do 116 | assert_raise Enum.EmptyError, fn -> Result.fold([], &Result.ok(&1 + &2)) end 117 | end 118 | 119 | test "fold/2 over a single value returns this value as ok value" do 120 | assert Result.fold(["foo"], fn _, _ -> Result.error("bar") end) == Result.ok("foo") 121 | end 122 | 123 | test "fold/2 over values returns last value returned by function if it returns only oks" do 124 | assert Result.fold([8, 9, 10], &Result.ok(&1 + &2)) == Result.ok(27) 125 | end 126 | 127 | test "fold/2 over values returns error when the function returns it" do 128 | assert Result.fold([8, 9, 10], fn 129 | _, 17 -> Result.error("edge of seventeen") 130 | x, y -> Result.ok(x + y) 131 | end) == Result.error("edge of seventeen") 132 | end 133 | 134 | test "oks/1 does nothing on empty list" do 135 | assert Result.oks([]) == [] 136 | end 137 | 138 | test "oks/1 returns ok value of an ok" do 139 | assert Result.oks([Result.ok(1)]) == [1] 140 | end 141 | 142 | test "oks/1 preserves ordering" do 143 | assert Result.oks([Result.ok(1), Result.ok(2)]) == [1, 2] 144 | end 145 | 146 | test "oks/1 doesn't return errors" do 147 | assert Result.oks([Result.error(1)]) == [] 148 | end 149 | 150 | test "oks/1 filters out only ok values" do 151 | assert Result.oks([Result.error(1), Result.ok(2)]) == [2] 152 | end 153 | 154 | test "all_ok/1 transforms list of results if all ok to result of list" do 155 | assert Result.all_ok([Result.ok(1), Result.ok(2)]) == Result.ok([1, 2]) 156 | end 157 | 158 | test "all_ok/1 returns first error" do 159 | assert Result.all_ok([Result.ok(1), Result.error(2)]) == Result.error(2) 160 | end 161 | 162 | test "all_ok/1 on empty list is ok([])" do 163 | assert Result.all_ok([]) == Result.ok([]) 164 | end 165 | 166 | test "to_maybe converts ok value to just value" do 167 | ok = Result.ok("foo") 168 | assert Result.to_maybe(ok) == Maybe.just("foo") 169 | end 170 | 171 | test "to_maybe converts error to nothing" do 172 | error = Result.error("bar") 173 | assert Result.to_maybe(error) == Maybe.nothing() 174 | end 175 | 176 | test "to_review converts ok value to accepted review" do 177 | ok = Result.ok("bar") 178 | assert Result.to_review(ok) == Review.accepted("bar") 179 | end 180 | 181 | test "to_review converts error with a list to rejected review with issues " <> 182 | "being the same list" do 183 | error = Result.error([1, 2, 3]) 184 | assert Result.to_review(error) == Review.rejected([1, 2, 3]) 185 | end 186 | 187 | test "to_review converts error that's no a list to rejected review with " <> 188 | "error value as a single issue" do 189 | error = Result.error("error") 190 | assert Result.to_review(error) == Review.rejected(["error"]) 191 | end 192 | end 193 | -------------------------------------------------------------------------------- /test/review_test.exs: -------------------------------------------------------------------------------- 1 | defmodule FE.ReviewTest do 2 | use ExUnit.Case, async: true 3 | doctest FE.Review 4 | 5 | alias FE.{Review, Result, Maybe} 6 | 7 | test "accepted can be created with a constructor" do 8 | assert Review.accepted(:baz) == {:accepted, :baz} 9 | end 10 | 11 | test "rejected can be created with a constructor" do 12 | assert Review.rejected([123]) == {:rejected, [123]} 13 | end 14 | 15 | test "issues can be created with a constructor" do 16 | assert Review.issues(:a, [:b, :c, :d]) == {:issues, :a, [:b, :c, :d]} 17 | end 18 | 19 | test "mapping over an accepted value applies function to value" do 20 | assert Review.map(Review.accepted("foo"), &String.length/1) == Review.accepted(3) 21 | end 22 | 23 | test "mapping over value with issues applies function to value" do 24 | assert Review.map(Review.issues("foo", [:a, :b, :c]), &String.length/1) == 25 | Review.issues(3, [:a, :b, :c]) 26 | end 27 | 28 | test "mapping over rejected returns rejected" do 29 | assert Review.map(Review.rejected([1, 2, 3]), &String.length/1) == Review.rejected([1, 2, 3]) 30 | end 31 | 32 | test "map_issues over an accepted review returns the same review" do 33 | assert Review.map_issues(Review.accepted("ack"), &String.length/1) == Review.accepted("ack") 34 | end 35 | 36 | test "map_issues over issues returns the same value and transformed issues" do 37 | assert Review.map_issues(Review.issues(:value, ["a", "bb", "ccc"]), &String.length/1) == 38 | Review.issues(:value, [1, 2, 3]) 39 | end 40 | 41 | test "map_issues over rejected returns rejected with transformed issues" do 42 | assert Review.map_issues(Review.rejected([1, 2, 3]), &(&1 * 3)) == Review.rejected([3, 6, 9]) 43 | end 44 | 45 | test "unwrap_or returns default value if rejected is passed" do 46 | assert Review.unwrap_or(Review.rejected([1]), :default) == :default 47 | end 48 | 49 | test "unwrap_or returns default value if issues are passed" do 50 | assert Review.unwrap_or(Review.issues(1, [2, 3]), "default") == "default" 51 | end 52 | 53 | test "unwrap_or returns wrapped value if accepted is passed" do 54 | assert Review.unwrap_or(Review.accepted("value"), "default") == "value" 55 | end 56 | 57 | test "unwrap! returns wrapped value if accepted is passed" do 58 | assert Review.unwrap!(Review.accepted("value")) == "value" 59 | end 60 | 61 | test "unwrap! raises an exception with issues in message if rejected is passed" do 62 | assert_raise Review.Error, "unwrapping rejected Review with issues: [:a]", fn -> 63 | Review.unwrap!(Review.rejected([:a])) 64 | end 65 | end 66 | 67 | test "unwrap! raises an exception with issues in message if issues are passed" do 68 | assert_raise Review.Error, "unwrapping Review with issues: [2, 3, 4]", fn -> 69 | Review.unwrap!(Review.issues(1, [2, 3, 4])) 70 | end 71 | end 72 | 73 | test "and_then returns accepted if accepted is passed and " <> 74 | "accepted is returned from function" do 75 | accepted = Review.accepted(3) 76 | 77 | assert Review.and_then(accepted, fn x -> Review.accepted(x * 3) end) == Review.accepted(9) 78 | end 79 | 80 | test "and_then returns issues if accepted is passed and " <> "issues are returned from function" do 81 | accepted = Review.accepted("bar") 82 | 83 | assert Review.and_then(accepted, fn "bar" -> 84 | Review.issues("baz", ["set too high"]) 85 | end) == {:issues, "baz", ["set too high"]} 86 | end 87 | 88 | test "and_then returns rejected if accepted is passed and " <> 89 | "rejected is returned from function" do 90 | accepted = Review.accepted(10) 91 | 92 | assert Review.and_then(accepted, fn x -> Review.rejected([x + 2]) end) == 93 | Review.rejected([12]) 94 | end 95 | 96 | test "and_then returns rejected if rejected is passed" do 97 | rejected = Review.rejected([123]) 98 | assert Review.and_then(rejected, fn x -> Review.accepted(x + 2) end) == rejected 99 | end 100 | 101 | test "and_then returns concat of issues if issues are passed and " <> 102 | "issues are returned from function" do 103 | issues = Review.issues(1, [1]) 104 | 105 | assert Review.and_then(issues, fn x -> Review.issues(x * 2, [x + 1]) end) == 106 | Review.issues(2, [1, 2]) 107 | end 108 | 109 | test "and_then returns issues and new value if issues are passed and " <> 110 | "accepted is returned from function" do 111 | issues = Review.issues(:foo, [:bar, :baz]) 112 | 113 | assert Review.and_then(issues, fn x -> 114 | Review.accepted(Atom.to_string(x)) 115 | end) == Review.issues("foo", [:bar, :baz]) 116 | end 117 | 118 | test "and_then returns rejected with collected issues " <> 119 | "if issues is passed and rejected is returned from function" do 120 | issues = Review.issues(:a, [:b, :c]) 121 | 122 | assert Review.and_then(issues, fn _ -> Review.rejected([:x]) end) == 123 | Review.rejected([:b, :c, :x]) 124 | end 125 | 126 | test "and_then chain collects issues on the way" do 127 | result = 128 | Review.accepted(1) 129 | |> Review.and_then(&Review.issues(&1, [&1 * 2, &1 * 3])) 130 | |> Review.and_then(&Review.accepted(&1 + 3)) 131 | |> Review.and_then(&Review.issues(&1 + 1, [&1 * 5, &1 * 4])) 132 | 133 | assert result == Review.issues(5, [2, 3, 20, 16]) 134 | end 135 | 136 | test "and_then chain stops on the first rejected and collects all issues on the way" do 137 | result = 138 | Review.accepted(1) 139 | |> Review.and_then(&Review.issues(&1, [&1 * 2])) 140 | |> Review.and_then(&Review.rejected([&1 * 3])) 141 | |> Review.and_then(&Review.issues(&1, [&1 * 4])) 142 | 143 | assert result == Review.rejected([2, 3]) 144 | end 145 | 146 | test "and_then cain returns last if there are no issues on the way" do 147 | result = 148 | Review.accepted(1) 149 | |> Review.and_then(&Review.accepted(&1 + 2)) 150 | |> Review.and_then(&Review.accepted(&1 * 3)) 151 | |> Review.and_then(&Review.accepted(&1 - 4)) 152 | 153 | assert result == Review.accepted(5) 154 | end 155 | 156 | test "fold/3 over an empty list returns whatever is passed as initial review" do 157 | f = fn _, _ -> Review.accepted(:qux) end 158 | assert Review.fold(Review.accepted(:foo), [], f) == Review.accepted(:foo) 159 | 160 | assert Review.fold(Review.issues(:bar, [1, 2]), [], f) == Review.issues(:bar, [1, 2]) 161 | 162 | assert Review.fold(Review.rejected([:baz]), [], f) == Review.rejected([:baz]) 163 | end 164 | 165 | test "fold/3 over a single value doesn't apply function if rejected is passed" do 166 | assert Review.fold(Review.rejected([:a]), [5], &Review.accepted(&1 + &2)) == 167 | Review.rejected([:a]) 168 | end 169 | 170 | test "fold/3 over a single value applies function if issues are passed" do 171 | assert Review.fold(Review.issues(1, [:a]), [2], &Review.accepted(&1 + &2)) == 172 | Review.issues(3, [:a]) 173 | 174 | assert Review.fold(Review.accepted(1), [5], &Review.accepted(&1 - &2)) == Review.accepted(4) 175 | end 176 | 177 | test "fold/3 over a single value applies function and collects issues" do 178 | assert Review.fold(Review.issues(1, [:a, :b]), [2], &Review.issues(&1 - &2, [:c, :d])) == 179 | Review.issues(1, [:a, :b, :c, :d]) 180 | end 181 | 182 | test "fold/3 over values returns last value returned by function if everything is accepted" do 183 | assert Review.fold(Review.accepted(1), [2, 3, 4], &Review.accepted(&1 * &2)) == 184 | Review.accepted(24) 185 | end 186 | 187 | test "fold/3 over values returns rejected when the function returns it" do 188 | assert Review.fold(Review.accepted(1), [2, 3, 4], fn 189 | _, 6 -> Review.rejected(["it's", "a", "six!"]) 190 | x, y -> Review.accepted(x + y) 191 | end) == Review.rejected(["it's", "a", "six!"]) 192 | end 193 | 194 | test "fold/3 over values collects issues returned by the function" do 195 | assert Review.fold(Review.accepted(1), [2, 3, 4, 5], fn 196 | x, 3 -> Review.issues(x + 3, ["three"]) 197 | x, 10 -> Review.issues(x + 10, ["ten"]) 198 | x, y -> Review.accepted(x + y) 199 | end) == Review.issues(15, ["three", "ten"]) 200 | end 201 | 202 | test "fold/2 over an empty list raises an EmptyError" do 203 | assert_raise Enum.EmptyError, 204 | fn -> Review.fold([], fn _, _ -> Review.accepted(:foo) end) end 205 | end 206 | 207 | test "fold/2 over a single value returns this value as an accepted review" do 208 | assert Review.fold([1], fn _, _ -> Review.rejected([:foo]) end) == Review.accepted(1) 209 | end 210 | 211 | test "fold/2 over values returns last value returned by function if every step is accepted" do 212 | assert Review.fold([1, 2, 3, 4, 5], &Review.accepted(&1 + &2)) == Review.accepted(15) 213 | end 214 | 215 | test "fold/2 over values returns rejected when the function returns it" do 216 | assert Review.fold([1, 2, 3, 4, 5], fn 217 | _, 10 -> Review.rejected(["ten", "TEN", "Ten"]) 218 | x, y -> Review.accepted(x + y) 219 | end) == Review.rejected(["ten", "TEN", "Ten"]) 220 | end 221 | 222 | test "fold/2 over values collects issues returned by function" do 223 | assert Review.fold([1, 2, 3, 4, 5], fn x, y -> Review.issues(x + y, [y]) end) == 224 | Review.issues(15, [1, 3, 6, 10]) 225 | end 226 | 227 | test "fold/2 over values returns collected issues when rejected is returned from function" do 228 | assert Review.fold([1, 2, 3, 4, 5], fn 229 | _, 10 -> Review.rejected([10]) 230 | x, y -> Review.issues(x + y, [y]) 231 | end) == Review.rejected([1, 3, 6, 10]) 232 | end 233 | 234 | test "to_result converts accepted value to an ok value" do 235 | accepted = Review.accepted(:baz) 236 | assert Review.to_result(accepted) == Result.ok(:baz) 237 | end 238 | 239 | test "to_result converts rejected to an error with all the issues" do 240 | rejected = Review.rejected([:a, :b, :c]) 241 | assert Review.to_result(rejected) == Result.error([:a, :b, :c]) 242 | end 243 | 244 | test "to_result converts issues to an error with all the issues" do 245 | issues = Review.issues(1, [:one, "one", 'one']) 246 | assert Review.to_result(issues) == Result.error([:one, "one", 'one']) 247 | end 248 | 249 | test "to_maybe converts accepted value to just value" do 250 | accepted = Review.accepted(:qux) 251 | assert Review.to_maybe(accepted) == Maybe.just(:qux) 252 | end 253 | 254 | test "to_maybe converts rejected to nothing" do 255 | rejected = Review.rejected(["d", "e", "f"]) 256 | assert Review.to_maybe(rejected) == Maybe.nothing() 257 | end 258 | 259 | test "to_maybe converts issues to nothing" do 260 | issues = Review.issues(2, [:two, "two", 'TWO']) 261 | assert Review.to_maybe(issues) == Maybe.nothing() 262 | end 263 | end 264 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------