Is this a good pattern for handling a list of {:ok|:error} results?

I’m parsing input, returning the successfully parsing items, and logging the parse errors. (In this app, this is the behavior I want: continue working with successful parses, and a log file with the errata.)

    processed_sections = map(raw_sections, &new_section/1)

    reduce(processed_sections, [], fn e, acc ->
      case e do
        {:error, msg} ->
          Logger.warn(msg)
          acc

        {:ok, section} ->
          acc ++ [section]
      end
    end)

Is there some more canonical way of working through the {:ok|:error} results?

You can get rid of the case statement with anonymous function pattern matching:

i.e.

[{1, 2}, {3}, {4, 5}]
|> Enum.reduce(0, fn
  {x, y}, acc -> acc + x + y
  {x}, acc -> acc + x
end)

or in your case

processed_sections = map(raw_sections, &new_section/1)

reduce(processed_sections, [], fn
  {:error, msg}, acc ->
    Logger.warn(msg)
    acc

  {:ok, section}, acc ->
    acc ++ [section]
end)
3 Likes

There are several ways to do this, but there is no “canonical” way.
Your solution has a problem with ++ operator, which makes this whole solution be O(n^2) complexity. I’d suggest prepending to the head and then reversing the result (or using Enum.flat_map)

3 Likes

Taking the opportunity for some bike shedding :slight_smile:… If you‘re not using the intermediate values, you could do it with some pipes:

raw_sections
|> map(&new_section/1)
|> reduce([], fn
  {:error, msg}, acc ->
    Logger.warn(msg)
    acc

  {:ok, section}, acc ->
    [section | acc]
end)
|> reverse()
2 Likes

I noticed how generic my code is, and thinking about moving this to a separate function like Haskell’s fromJust or catMaybes: Data.Maybe (Does Elixir have a similar library?)

So, e.g.,

catOks(processed_sections, &Logger.warn/1)

Yeah, that’s pretty good. And then I’d just refactor it by extracting the function:

raw_sections
|> map(&new_section/1)
|> catOks(&Logger.warn/1)

Does Elixir have a library with functions like catOks? (Searching…)

I‘m not aware of one in the standard lib, but you already have most of it :slight_smile:

def cat_oks(list, fun) do
  list
  |> reduce([], fn
    {:error, msg}, acc ->
      fun.(msg)
      acc

    {:ok, section}, acc ->
      [section | acc]
  end)
  |> reverse()
end

Using ’Enum.filter/2’ would save you the need to reverse, though:

def cat_oks(list, fun) do
  list
  |> filter(fn
    {:error, msg} ->
      fun.(msg)
      false

    {:ok, section} ->
      true
  end)
end

Edit: Ah, sorry, disregard the above, it would not unwrap the tuples.

1 Like

I tend to prefer functions other than custom reduce, so in this case, I would probably do someting like this:

{successful, failed} = Enum.split_with(processed_sections, &match?({:ok, _}, &1))

Enum.each(failed, fn {:error, msg} -> Logger.warn(msg) end)

successful

Seriously though, we need a standardized higher level way to work with :ok and :error tuples.

5 Likes

Does Elixir have a similar library?

Towel (or specifically its recentmost fork) has it IIRC. Alternatively, calling Gleam’s result module would do the same. I have a personal helpers lib (a couple of them) with a whole bunch of utilities like this, but it’s not in the state where it would make sense to publish it. Anyways it wouldn’t take one more than a day or two to come up with one.

This can be golfed down even further by using Enum.flat_map to express the “map, but only keep some of them” idiom:

raw_sections
|> map(&new_section/1)
|> Enum.flat_map(fn
  {:ok, result} -> [result]
  {:error, msg} -> Logger.warn(msg); []
end)

Monad enthusiasts should be able to spot Result being transformed into Option there :stuck_out_tongue:

4 Likes

Options are not about binding/flatmapping lists, an Option type would need to be represented as something like {:some, a} | :nothing, or did I misunderstand what you meant?

I think he meant that the inner function produces an Option, structurally speaking. A Just or a Nothing.

I forget about flat_map and it’s great for this task. It embodies the “map, but maybe only keep some”, like you say. So as the coder, I’m on alert as to what’s happening. Vis-a-vis reduce which is totally open-ended structure creation. And so its presence doesn’t give any kind of ‘hint’ to the reader.

1 Like

Nice, so updating with flat_map, my current extracted function is:

  def cat_oks(list, fun) do
    list
    |> Enum.flat_map(fn
      {:ok, result} -> [result]
      {:error, msg} -> fun.(msg); []
    end)
  end
1 Like

Since this is a “good pattern” thread… shouldn’t the name of the function represent that it’s also doing something with errors rather than just filtering for oks? :slight_smile:

What @dogweather said, though I’d spell it like this:

  • [x] is Some(x)
  • [] is None

This matches the behavior of Option in Rust’s implementation.

1 Like

Interesting, I haven’t yet seen implementations of Option in terms of lists, only in terms of tagged unions. Thanks for pointing out!

I guess people hate typing out those pesky tuples but I like having to do it (and don’t super love options types) because it doesn’t ‘hide’ the underlying data structure and thus makes reading the code have less mental overhead.

2 Likes