Noether aims to ease common data manipulation tasks by introducing simple algebraic functions and other utilities. Functions and names are inspired (sometimes taken as-is) from Haskell.
The Maybe
module introduces operations on nullable values.
The Either
module introduces operations on {:ok, _} | {:error, _}
values.
The List
module introduces operations on lists.
The root module has a few simple functions one might find of use.
def deps do
[
{:noether, "~> 1.0.0"}
]
end
Here is a list of real world scenarios where you may find that using constructs like Maybe
and Either
make your code less verbose, more straightforward, and easier to read.
Suppose you have a function that returns a list of items, and you want to take the first element (if the list is not empty), apply a function to it, and wrap it in a nice {:ok, _}
or {:error, _}
tuple.
Without Noether, you would write something like this:
function_that_returns_list_of_items()
|> List.first()
|> update_item(&f/1)
|> case do
nil ->
{:error, :not_found}
item ->
{:ok, item}
end
defp update_item(nil), do: nil
defp update_item(item, f), do: f.(item)
That's kind of verbose, especially since you need to type the functions that pattern match on nil
and those that wrap a result in a tuple. Moreover, what if function_that_returns_list_of_items
does not return just a list, but it may return an error as well? That's another case do
!
Let's see how we could accomplish the same with Noether:
alias Noether.Maybe
function_that_returns_list_of_items()
|> List.first()
|> Maybe.map(&f/1)
|> Maybe.required(:not_found)
Maybe
operates on nullable values, while Either
operates on {:ok, _}
or {:error, _}
tuples. Let's see how we can reduce the verbosity of elixir with
operator using Either.bind/2
.
Suppose you have N chained calls to different functions, where each one may return a tuple, and finally you want to return the "unwrapped" result to the caller. Normally, you would accomplish it this way:
with {:ok, _res1} <- f1(),
{:ok, _res2} <- f2(),
{:ok, _res3} <- f3(),
{:ok, res4} <- f4() do
res4
end
It can easily get frustrating and error-prone to write everytime the same {:ok, _}
matches. Let's see how we can do this using Noether:
alias Noether.Either
alias Noether.List
[f1(), f2(), f3(), f4()]
|> List.sequence()
|> Either.unwrap()
Easier to read, less verbose, and it encapsulates the handling of {:ok, _}
tuples. You can focus on writing actual logic instead of repeating the same pattern matches every time.
Feel free to propose any function you deem useful and even vaguely related to the ones currently present.
Regarding naming, we have a couple of conventions:
- function names should be taken from Haskell if they exist, aliases with Scala naming are possible (e.g.
bind
is aliased intoflat_map
) - function arguments are named
a, b, c ...
if values (exception:default
),f, g, h ...
if functions
mix test
runs the tests.
mix format.all
formats all the files under lib/
.
mix check
checks if the files are formatted; it then runs a linter (credo
) and a type checker (dyalixir
).