Towel 
A delightfully simple Maybe monad library that's written for Elixir.
Installation
Keep calm and add Towel to your mix.exs dependencies:
def deps do
[{:towel, "~> 0.1.0"}]
end
and run $ mix deps.get.
You're ready to start righting more expressive code!
Philosophy
Let's keep it Elixir. Forget Haskell's Just and Nothing; Elixir's already got its tuple-based {:ok, value} and {:error, reason} construct, so let's use that. That's why Towel's Maybe monad returns {:ok, value} and {:error, reason} instead of :some or :none or whatnot.
Elixir's got piping (|>). Piping is great, but not when you've got wrapped values. Keep your towel on you:
ok([answer: 42])
|> fmap(&Map.get(&1, :answer))
# {:ok, 42}
Quick Start
What if you're going to run into an error along the way? Say you want to travel through a map, but aren't sure whether or not a key is going to have a value.
map = %{system: "sol", z: %{z: %{nine: %{plural: %{z: %{alpha: "earth"}}}}}}
path = [:z, :z, :nine, :plural, :z, :alpha]
planet = List.foldl path, wrap(map), fn key, m ->
fmap(m, &Map.get(&1, key))
end
# {:ok, "earth"}
Woah! Did that just work?? Yes it did. But what if our path list was messed up and we didn't have a proper path? What if it was like this?
map = %{system: "sol", z: %{z: %{nine: %{plural: %{z: %{alpha: "earth"}}}}}}
path = [:z, :z, :ten, :plural, :z, :alpha]
planet = List.foldl path, wrap(map), fn key, m ->
fmap(m, &Map.get(&1, key))
end
# {:error, nil}
Look at that! You didn't even need a dolphin and your code safely ran without raising an error. Let's take a look at our functions list.
Tour of the Galaxy Library
Note: When you use Towel, all the following functions are added to the global namespace.
Maybe
wrap/1 The Elixir version of Haskell's Monad.return. Whatever you pass into it, whether it be a pre-existing monad or a regular, unwrapped value, it will return a value wrapped in an ok. If nil or :error is passed into it, it will return an error.
wrap(nil) # {:error, nil}
wrap(:error) # {:error, nil}
wrap(true) # {:ok, true}
wrap(false) # {:ok, false}
wrap("mice") # {:ok, mice}
unwrap/1 does the opposite—it'll give you your bare value, no matter how many times it may have been wrapped by a maybe.
unwrap({:ok, {:ok, {:ok, 42}}}) # 42
unwrap(nil) # nil
unwrap({:error, "no point"}) # "no point"
ok/1 wraps whatever you pass into it in an ok. For when you want to explicitly state that the wrapped value is, in fact, OK.
error/0, error/1 wraps whatever you pass into it in an error. For when you want to explicitly state that there's a problem with the error.
combine/1 takes a list of monads and reduces them into either {:error, reason} if there was an error in the list, or {:ok, values}, where values is a list of all the wrapped values, in the order they were passed in.
case combine([network_request(params), db_request(query)]) do
{:ok, [response, record]} ->
# do stuff
{:error, reason} ->
Logger.debug("There was a problem. What kind of problem? I don't know.")
end
Monad
bind/2 takes a monad and a function, and applies the function with the wrapped value of the monad if the monad is ok. Silently passes if the monad is an error. Expects the return value of the function to be either {:ok, value} or {:error, reason}. You can use Maybe.ok and Maybe.error instead.
File.read("inspirational_quotes.txt")
|> bind(fn quotes ->
if length(quotes) > 0 do
ok(quotes)
else
error("life's depressing.")
end
end)
Functor
fmap/2 takes a monad and a function, and applies the function with the wrapped value of the monad if the monad is ok. Silently passes if the monad is an error. Will automatically wrap the return value in a maybe by passing the return value to Maybe.wrap.
File.read("inspiration_quotes.txt")
|> fmap(fn quotes ->
if length(quotes) > 0, do: quotes, else: nil
end)
Usage
Just add use Towel to the top of whatever module you're working in first (or just run it in iex to try it out):
defmodule Hitchhiking.Galaxy do
# Don't panic
use Towel
end
Is it any good?
Yes.
License
Copyright (c) 2015 Kash Nouroozi
This work is free. You can redistribute it and/or modify it under the terms of the MIT License. See the LICENSE file for more details.