Is there a better way to handle :ok tuples?

Exactly the same here. I’ve ended up with {:ok, value} and {:error, %SomeException{}}, though I run everything through a normalize function that fixes things up well. This really is the Maybe/Either type in static typed languages and we’ve ended up just doing monad transformations on them, that’s essentially what with is, just a monadic do but a bit more verbose. Language support for it all would be awesome. :slight_smile:

2 Likes

Shhh, don’t tell anyone! Monads are scary! :wink:

We use this pattern quite a lot as well. Unfortunately, these values do not work so great in function pipelines. Usually its not a problem - often pipelines don’t need to explicitly handle failure, the consumers can match on the result, or they have that baked in to the data structure you are piping like with an Ecto.Multi. I have a utility function I employ though when I need maybe just a single non-result handler (a function that takes an untagged value) in the middle of a pipeline. Its just fmap (but don’t tell anyone, Functors are scary!):

  @type result :: {:ok | :error, any}

  @spec map_ok(result, (any -> any)) :: result
  def map_ok({:ok, value}, fun), do: {:ok, fun.(value)}
  def map_ok({:error, _} = error, _fun), do: error

You can employ it like so:

starting_value
|> result_producer()
|> map_ok(&non_result_consumer/1)
3 Likes

There are a variety of libraries that give you something like ~> instead of |> to do a ‘monadic pipe’ though, I use the exceptional library for example for that (as it handles a variety of such things). ^.^;

starting_value
|> result_producer()
~> non_result_consumer()

Or you can use exceptional’s ~> at all points regardless:

starting_value
~> result_producer()
~> non_result_consumer()

It has an option to override |> directly, which should be ‘generically’ safe except macro work and so forth (plus overriding built-in things is eh anyway), so it doesn’t do it by default (and suggests against it).

2 Likes