README.md

# OK then...

**The Swiss Army Knife for tagged tuple pipelines**

[![Hex pm](https://img.shields.io/hexpm/v/ok_then)](https://hex.pm/packages/ok_then)
[![Documentation](https://img.shields.io/badge/docs-hexdocs-blue)](https://hexdocs.pm/ok_then)
[![Test Status](https://img.shields.io/github/workflow/status/flexibility-org/ok_then/Mix%20Tests)](https://github.com/flexibility-org/ok_then/actions)

Looking for a better way to handle **errors**, **optional results**,
and **default values**? Wish there were a really **consistent** and
**full-featured** API to handle **tagged tuples** in **pipelines**?

* [How to install](#installation)
* [Why does this exist?](#why)
* [API Documentation](https://hexdocs.pm/ok_then/OkThen.Result.html)
* [Elixir Forum Thread](https://elixirforum.com/t/39983)

## At a glance

**Wrap values:**
```elixir
Result.from("hello")     # {:ok, "hello"}
Result.from(1)           # {:ok, 1}
Result.from(nil)         # :none
Result.from_error(1)     # {:error, 1}
Result.from_as(1, :some) # {:some, 1}
```

**Map values selectively by tag:**
```elixir
def pipeline(value_r) do
  value_r
  |> Result.map(& &1 * 2)
  |> Result.error_map(& {:bad_input_value, &1})
  |> Result.tagged_map(:add_2, & &1 + 2)
end

Result.from(1) |> pipeline()            # {:ok, 2}
Result.from(nil) |> pipeline()          # :none
Result.from_error(1) |> pipeline()      # {:error, {:bad_input_value, 1}}
Result.from_as(1, :add_2) |> pipeline() # {:add_2, 3}
```

**Or apply functions selectively by tag:**
```elixir
def double(value), do: {:ok, value * 2}
def error(value), do: {:error, value}

{:ok, 1} |> Result.then(&double/1) |> Result.then(&double/1)      # {:ok, 4}
{:ok, 1} |> Result.then(&error/1) |> Result.then(&double/1)       # {:error, 1}
{:ok, 1} |> Result.then(&double/1) |> Result.then(&error/1)       # {:error, 2}
{:ok, 1} |> Result.then(&error/1) |> Result.error_then(&double/1) # {:ok, 2}
```

**And handle unexpected values safely:**
```elixir
def to_nil(_value), do: nil
def error(value), do: {:error, value}

def unwrap_result(result) do
  Result.default("default")
  |> Result.unwrap_or_else("failsafe")
end

Result.from(1)           # {:ok, 1}
|> unwrap_result()       # 1

Result.from(1)           # {:ok, 1}
|> Result.map(&to_nil/1) # :none
|> unwrap_result()       # "default"

Result.from(1)           # {:ok, 1}
|> Result.then(&error/1) # {:error, 1}
|> unwrap_result()       # "failsafe"
```

**Typespecs:**
```elixir
@spec a() :: Result.ok_or(any())
@spec a() :: :ok | {:error, any()}

@spec b() :: Result.ok_or(integer(), any())
@spec b() :: {:ok, integer()} | {:error, any()}

@spec c() :: Result.maybe(integer())
@spec c() :: {:ok, integer()} | :none

@spec d() :: Result.maybe(integer(), any())
@spec d() :: {:ok, integer()} | :none | {:error, any()}
```

**You can even handle tagged tuples inside Enums:**
```elixir
# Tags other than :ok and :error are supported too :)
[{:ok, 1}, {:ok, 2}, {:ok, 3}, {:error, 4}, {:error, 5}]
|> Result.Enum.group_by_tag()
%{
  error: [4, 5],
  ok: [1, 2, 3]
}
```

```elixir
[{:ok, 1}, {:ok, 2}, {:ok, 3}]
|> Result.Enum.collect()
{:ok, [1, 2, 3]}
```

```elixir
[{:ok, 1}, {:ok, 2}, {:error, 3}, {:ok, 4}]
|> Result.Enum.collect()
{:error, 3}
```

Check out [the API documentation](https://hexdocs.pm/ok_then/OkThen.Result.html)
for a full list of supported functions, guards, and types.

## Why?

Because:

1. Remembering to **check for `nil`** is the bane of any programmer's life. It
   pops up _everywhere_.
2. **Tagged tuples**, the idiomatic solution to this problem, can become
   **verbose**, especially when they need to be passed to several functions, or
   through a pipeline.
3. Although `{:ok, value} | {:error, reason}` is ubiquitous, there is no
   standardised pattern to represent **optional values**, other than `value |
   nil`.

Failing to address point 3 leads to code that **either**:

1. Returns `{:ok, nil}`, which brings us **right back** to unexpected `nils`
   popping up in the most obscure ways:

   `** (UndefinedFunctionError) function nil.my_map_key/0 is undefined.`

2. Returns `{:error, :not_found}` or similar, which is often semantically
   questionable: missing values are often **not actually errors**. This can lead
   to confusion in determining how best to handle **fallback values** and error
   logging.

One solution (adopted by languages such as Rust), is to provide a return type
that is **explicitly optional**. In Elixir we could represent this with an
orthogonal type of tagged tuple:


```elixir
{:some, value} | :none
```

The main drawback of this approach is how verbose the tuples can become in log
output, especially when nested.

```elixir
{:ok, {:some, %MyStruct{key: "value"}}}
{:ok, :none}
{:error, {:some, "Example Error"}}
{:error, :none}   # Is this an error, or a lack of error?
```

To mitigate this issue, the approach taken by this package is to **combine** the
"ok/error" and "some/none" types into a single type of tagged tuple called a
**"maybe"**:

```elixir
{:ok, value} | :none | {:error, reason}
```

* **Specific functions** are provided to handle tagged tuples with these tags (`:ok,
  :none, :error`).
* **Generic functions** also exist to handle _any_ tag, but are slightly less
  convenient.
* Functions that _create_ or _map_ tagged tuples will **catch `nil` values** and
  transform the returned result into `:none`.

This time, we can handle an unexpected `nil` far more elegantly. Here is a
slightly contrived example:

```elixir
get_my_map()                           # nil
|> Result.from()                       # :none
|> Result.default(%{})                 # {:ok, %{}}
|> Result.map(&Map.get(:my_map_key))   # :none
|> Result.unwrap_or_else("default")    # "default"
```

And now imagine we could introduce an unexpected error. This is not altered by
`default`, which only affects results tagged `:none`. However, `unwrap_or_else`
will catch _any_ result that is not `:ok`:

```elixir
{:error, "Unexpected Error"}
|> Result.default(%{})                 # {:error, "Unexpected Error"}
|> Result.map(&Map.get(:my_map_key))   # {:error, "Unexpected Error"}
|> Result.unwrap_or_else("default")    # "default"
```

Even better, we could "consume" the error by logging it, then handle the missing
value just like before:

```elixir
{:error, "Unexpected Error"}
|> Result.error_consume(&Logger.error/1) # :none ("Unexpected Error" is logged)
|> Result.default(%{})                   # {:ok, %{}}
|> Result.map(&Map.get(:my_map_key))     # :none
|> Result.unwrap!()                      # "default"
```

Take a look at [some more examples](#at-a-glance).

## Installation

Simply add the package to your deps in `mix.exs`:

```elixir
def deps do
  [
    {:ok_then, "~> x.x.x"}  # Check "hex" badge at the top for current version
  ]
end
```