Pronouncing `<-`

How about this explanation:

You know Elixir’s pipe |> operator I presume, it is basically defined like this (but as a larger macro/specialForm/etc…):

def value |> fun, do: fun(value) # Except as a macro, but conceptually it is this simple

Well a monad ‘bind’ operator (in Haskell the >>= operator by default) is basically just this when elixirized:

# An :ok/:error tuple monad
def {:ok, value} >>= fun, do: fun(value)
def {:error, _value}=err >>= _fun, do: err
# And whatever else you want, any kind of container (or in fact no container) are monads
# A map is a monad, a user struct is a monad, a list is a monad, etc...

Technically the Elixir normal pipe of |> is a monad ‘bind’ as well, but just for the simple unwrapped value (hence why it is strictly less useful than a monad pipe while a monad pipe can still do what the elixir pipe can do). :slight_smile:

Examples

Let’s demonstrate with some examples:

Normal piping (naked integer):

def add(a, b), do: a+b

2
|> add(2) # returns 4
|> add(3) # returns 7

Monad piping (let’s use, oh, the error/success tuple):

def good_add(a, b), do: {:ok, a+b}
def bad_add(a, b), do: {:error, "I borked!"}

good_add(2, 2) # returns {:ok, 4}
>>= good_add(3) # returns {:ok, 7}
>>= bad_add(4) # returns {:error, "I borked!"}
>>= good_add(5) # returns {:error, "I borked!"}

Basically the only difference between the Elixir pipe |> operator and a monad bind >>= operator (traditionally >>> or ~> in Elixir libraries due to syntax restrictions in Elixir) is that the monad bind ‘unwraps’ the value from it’s container before passing it to the function (then of course the function is ‘should’ build a new container for it and return it, but not really necessary in all cases, like for converting from one form to another, like there is a function traditionally called ‘return’ that just unwraps the value and returns it with no container).

Any more clear I hope? :slight_smile:

If you are curious, there are a whole host of libraries for Elixir that adds monads (and optionally many can also ‘enhance’ the traditional Elixir pipe to work as a monad bind, so they container to work as normal but can also work with, say, ok/error tuples and so forth). My favorite is @expede’s witchcraft, which is a full-fledge monad handling library with a lot more (it contains about everything you need, designed to work with the default monad ‘definitions’ in her other libraries like algae, and built on the very low level library also made by expede of quark while the new version is also built on the amazing library of type_class, also made by expede, that adds typeclasses to Elixir in a fascinating and succinct way). @expede also made the exceptional library, which is basically ‘just’ a monad handler ‘just’ for success/error typing (the number one fault that normal elixir piping does not handle), and it handles ok/error tuples, exceptions, and more, it let’s you turn stuff like:

try do
  2
  |> i_might_throw!()
  |> i_return_ok_or_error_tuple()
  |> case do
    {:error, _err}=error -> error
    {:ok, value} ->
      value
      |> do_something()
      |> i_also_return_ok_or_error_tuple()
      |> case do
        {:error, _err}=error -> error
        {:ok, value} ->
          value
          |> more_work_that_can_only_be()
          |> done_after_confirming_the_tuple()
          |> was_a_success_and_unwrapping_it()
      end
  end
rescue
  exc -> exc
end

Well, that is a bit of hell, but entirely expected code considering normal elixir and libraries and erlang libraries and all, it is a mess and the pipe operator is not up to the task. At this point for readability (and sanity) you’d break it up into intermediate variables or more function, which just makes the code huge. With exceptional it could instead just be:

2
|> safe(&i_might_throw!(&1)).()
~> i_return_ok_or_error_tuple()
~> do_something()
~> i_also_return_ok_or_error_tuple()
~> more_work_that_can_only_be()
~> done_after_confirming_the_tuple()
~> was_a_success_and_unwrapping_it()

Oh look, a normal piping chain but using ~> instead of |> where you want to unwrap the value or pass the error, and this is just the default mode to use it, you can have it enhance the pipe operator so it just becomes:

2
|> safe(&i_might_throw!(&1)).()
|> i_return_ok_or_error_tuple()
|> do_something()
|> i_also_return_ok_or_error_tuple()
|> more_work_that_can_only_be()
|> done_after_confirming_the_tuple()
|> was_a_success_and_unwrapping_it()

Though this is not the default mode or even recommended mode except in very tightly controlled areas as it does change the expectation of how the normal pipe operator works (I wish it worked like this, how often do you want to pass an ok/error tuple to a function instead of just it’s success value). And no, those last three it does not matter if they return raw values, ok/error tuples, return exceptions (though if they raise you need to wrap it with a ‘safe’), it all ‘just works’.

It also has things to setup alternate paths for error piping and such too as well as for raising an error condition as an exception or normalizing it to an ok/error tuple or as a naked value or exception. It is a really nice and useful library that makes piping into monad handling for errors, and by doing so it makes handling ok/error conditions so much easier and I wish something like it was built in to Elixir so people stopped doing error handling so crazy (with the exceptional library the ! functions become entirely useless as one example, I hate hate hate duplicate functions that only differ in how they handle error like that!).

4 Likes