Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 0 additions & 24 deletions lib/noether/either.ex
Original file line number Diff line number Diff line change
Expand Up @@ -24,27 +24,6 @@ defmodule Noether.Either do
def map({:ok, a}, f) when is_function(f, 1), do: {:ok, f.(a)}
def map(a = {:error, _}, _), do: a

@doc """
Given a `value` and a function, it applies the function on the `value` returning `{:ok, f.(value)}`.
If the function throws an exception `e` then it is wrapped into an `{:error, e}`.

## Examples

iex> try("42", &String.to_integer/1)
{:ok, 42}

iex> try("nan", &String.to_integer/1)
{:error, %ArgumentError{message: "errors were found at the given arguments:\\n\\n * 1st argument: not a textual representation of an integer\\n"}}
"""
@spec try(any(), fun1()) :: either()
def try(value, f) when is_function(f, 1) do
try do
{:ok, f.(value)}
rescue
e -> {:error, e}
end
end

@doc """
Given an `{:ok, {:ok, value}}` it flattens the ok unwrapping the `value` and returning `{:ok, value}`.
If an `{:error, _}` is given, it is returned as-is.
Expand All @@ -54,9 +33,6 @@ defmodule Noether.Either do
iex> join({:ok, {:ok, 1}})
{:ok, 1}

iex> join({:ok, 1})
Comment thread
sphaso marked this conversation as resolved.
** (FunctionClauseError) no function clause matching in Noether.Either.join/1

iex> join({:error, "Value not found"})
{:error, "Value not found"}
"""
Expand Down
100 changes: 100 additions & 0 deletions lib/noether/try.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
defmodule Noether.Try do
Comment thread
cottinisimone marked this conversation as resolved.
@moduledoc """
This module hosts several utility functions to work with `{:ok, _} | {:error, _}` values.

"""
@type either :: {:ok, any()} | {:error, any()}
@type fun0 :: (-> either())
@type fun1 :: (any() -> any())
@type fune :: (any() -> either())

@doc """
Given a function, it runs the function returning the result of `f.()`. `f` is expected
to be a function that returns an `either` value.
If the function throws an exception `e` then it is wrapped into an `{:error, e}`.

## Examples

iex> run(fn -> {:ok, 42} end)
{:ok, 42}

iex> run(fn -> String.to_integer("nan") end)
{:error, %ArgumentError{message: "errors were found at the given arguments:\\n\\n * 1st argument: not a textual representation of an integer\\n"}}
"""
@spec run(fun0()) :: either()
def run(f) when is_function(f, 0) do
try do
f.()
rescue
e -> {:error, e}
end
end

@doc """
Given a `value` and a function, it applies the function on the `value` returning `{:ok, f.(value)}`.
If the function throws an exception `e` then it applies the error function wrap the exception into an `{:error, e}`.

## Examples

iex> map("42", &String.to_integer/1)
{:ok, 42}

iex> map("nan", &String.to_integer/1)
{:error, %ArgumentError{message: "errors were found at the given arguments:\\n\\n * 1st argument: not a textual representation of an integer\\n"}}
"""
@spec map(any(), fun1()) :: either()
def map(value, f) when is_function(f, 1) do
try do
{:ok, f.(value)}
rescue
e -> {:error, e}
end
end

@doc """
Given a `value` and two function, it applies the ok function on the `value` returning `{:ok, f.(value)}` if no exception is thrown.
If the ok function throws an exception `e` then it applies the error function wrap the exception into an `{:error, e}`.

## Examples

iex> try_or_else("42", &String.to_integer/1, fn e -> {:error, e} end)
{:ok, 42}

iex> try_or_else("nan", &String.to_integer/1, fn e -> {:error, e} end)
{:error, %ArgumentError{message: "errors were found at the given arguments:\\n\\n * 1st argument: not a textual representation of an integer\\n"}}
"""
@spec try_or_else(any(), fun1(), fune()) :: either()
def try_or_else(value, f, g \\ fn e -> {:error, e} end)
when is_function(f, 1) and is_function(g, 1) do
try do
{:ok, f.(value)}
rescue
e -> g.(e)
end
end

@doc """
Applies `f` to the given `value`, returning `{:ok, f.(value)}` when no exceptional control flow happens.
Differently from `map/2`, this function uses `catch`, so it also intercepts `throw/1` (and other low-level
exits) and wraps the intercepted term inside `{:error, _}`.

## Examples

iex> recover(5, fn value -> value + 1 end)
{:ok, 6}

iex> recover(:boom, fn _ -> throw(:boom) end)
{:error, :boom}

iex> recover(:fatal, fn _ -> exit(:fatal) end)
{:error, :fatal}
"""
@spec recover(any(), fun1()) :: either()
def recover(value, f) when is_function(f, 1) do
try do
{:ok, f.(value)}
catch
_type, reason -> {:error, reason}
end
end
end
5 changes: 5 additions & 0 deletions test/noether/try_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
defmodule Noether.TryTest do
use ExUnit.Case
doctest Noether.Try, import: true
use ExUnitProperties
end