With do(:)?

I would argue that they require quite a different mindset to absorb - a mindset that is not fostered when primarily doing rote imperative programming work. During my university education I had to maintain some modicum of mathematical competence in order to complete the curriculum. Sadly that skillset was never challenged (and therefore maintained) during subsequent professional work. What I’m saying is that monads are abstract in a mathematical way, which is (at least to me) a different kind of abstract that you are dealing with when you design your in-the-wild software solutions.

So for the time being getting a more intuitive sense of what “monads” are really about is an ongoing process for me.

It was during my intensified exposure to pattern matching with Erlang that I started to see the relationship of Option to ({:ok, value} | :error) and Either (or Result) to ({:ok, value} | {:error, reason}). Shortly thereafter I looked at Kernel.with/1 and all of a sudden bind popped into my head.

The point I’m trying to make is that I really like those ResultMonad, OptionMonad, ListMonad modules - but I also still have enough of a beginner’s mind to realize that they are going to look absolutely alien to Elixir neophytes who don’t have any pre-exposure. (I still hate the name bind because it never helped me uncover the mystery behind it - but it’s not like I have a more descriptive name for it).

Now one has to be careful, while beginner friendliness is important to foster adoption, it can go overboard (I’m looking at you JavaScript).

But in terms of fostering understanding why this approach is a good idea it may be necessary to “fill-in” some much more pedestrian steps in between (not for production use - just for learning/understanding).

For example from

content =
  Enum.find_value(sources, fn source ->
    File.exists?(source) && File.read!(source)
  end) || raise "could not find #{source_file_path} in any of the sources"

to

{:ok, content} =
  Enum.find_value(sources, &Option.from_result(File.read(&1)))
  ~> fn _ -> raise "could not find #{source_file_path} in any of the sources" end
  # Or do `|> unwrap()` here and replace the binding above with just `content =`

there is still the danger of this happening.

So when I approached this in a more pedestrian manner I arrived at this:

defmodule M do

  def bind_option({:ok, value}, fun), do: fun.(value)
  def bind_option(:error, _fun), do: :error

  def unwrap_option({:ok, value}, _on_err), do: value
  def unwrap_option(:error, on_err), do: on_err.()

  def bind_result({:ok, value}, fun), do: fun.(value)
  def bind_result({:error, _reason} = err, _fun), do: err

  def unwrap_result({:ok, value}, _on_err), do: value
  def unwrap_result({:error, reason}, on_err), do: on_err.(reason)

  def some_fun do
    source_file_path = "whatever"
    sources = []

    on_err = fn (_reason) ->
      raise "could not find #{source_file_path} in any of the sources"
    end

    sources
    |> Enum.find_value({:error, :enoent}, &File.read/1)
    |> unwrap_result(on_err)
  end
end

Now unfortunately this example doesn’t benefit from bind directly but I think the juxtaposition of bind and unwrap both using pattern matching helps towards building a more intuitive sense towards what bind is all about (and understanding what those modules are about) - provided one has already accepted pattern matching as part of the everyday toolbox (something that I find one is driven to in Erlang - not so much in Elixir).

Ultimately I find that bind makes these ubiquitous tagged tuples more “pipe friendly” (and Elixir developers love their pipes).

PS: I still believe that the readability of the following (alternate) code suffers more from the inline definition of the anonymous function than the if conditional - I would find Enum.search(sources, &get_file/1) much more readable.

res = 
  Enum.search(sources, fn source ->
    if File.exists?(source) do
      {:ok, File.read!(source)}
    else
      :error
    end
  end) 
content =
  case res do
    :error -> raise "could not find #{source_file_path} in any of the sources"
    {:ok, content} -> content
  end

And I still cannot warm up to using either Kernel.&&/2 nor Kernel.||/2 - how would you even spec’ that in an “intention revealing” manner? This (non-valid) pseudo-spec

@spec || (l,r) :: r when l: (false|nil), r: as_boolean(term())
@spec || (l,_) :: l when l: as_boolean(term())

@spec && (l,_) :: l when l: (false|nil)
@spec && (_,r) :: r when r: as_boolean(term())

makes my skin crawl.

I felt this type of usage was a bad idea when I first encountered it in JavaScript and I haven’t come across an argument yet to convince me otherwise.

1 Like