Enum.strain and Stream.strain

64 views
Skip to first unread message

Spencer Carlson

unread,
Aug 20, 2019, 11:21:02 PM8/20/19
to elixir-lang-core
Given the

{ok, result} | {:error, result}

convention and the beautiful nature of piping, I often find myself looking for an elegant way to "strain" out all of the {:error _} results in the middle of a pipe so I can continue processing the results that were successful. I know the Enum and Stream modules already have a lot of functions but I think a strain function would add significant value especially in overall code readability.

Here is a possible source example

def strain(enumerable) do
   
Enum.reduce(enumerable, [], fn
     
{:ok, tuple}, acc when is_tuple(tuple) ->
       
case tuple do
         
{:ok, result} -> [result | acc]
         
{:error, _} -> acc
       
end
     
{:ok, result}, acc -> [result |  acc]
      _
, acc -> acc
   
end)
 
end

And an overloaded version that allows for handling errors:

def strain(enumerable, fun) do
   
Enum.reduce(enumerable, [], fn
     
{:ok, tuple}, acc when is_tuple(tuple) ->
       
case tuple do
         
{:ok, result} -> [result | acc]
         
{:error, result} ->
            fun
.(result)
            acc
       
end
     
{:ok, result}, acc -> [result |  acc]
     
{:error, result}, acc ->
          fun
.(result)
          acc
      _
, acc -> acc
   
end)
 
end


This would allow client code to look like this:

results = list
|> Enum.map(&do_something/1)
|> Enum.strain()
|> Task.async_stream(&do_another_expensive_thing/1)
|> Enum.strain()
|> Enum.to_list()


Simple examples (for clarity)
iex> Enum.strain([{:ok, "good job"}, {:error, "bad input"}])
["good job"]


iex
> Enum.strain([{:error, "bad input"}], &IO.inspect/1)
"bad input"
[]


Thanks,
Spencer Carlson



Chris McCord

unread,
Aug 20, 2019, 11:53:01 PM8/20/19
to elixir-l...@googlegroups.com
While {:ok, result} | {:error, reason} is ubiquitous, it isn’t always so strict, and the :ok and :error tuples often deviate in length for added information, such as Ecto multi results. I think the existing filters work quite well, so I wouldn’t add it to std lib:

  |> Enum.filter(res -> match?(res, {:ok, _}) end)

  # or 

  |> Enum.filter(&match?(&1, {:ok, _}))


Chris


--
You received this message because you are subscribed to the Google Groups "elixir-lang-core" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-co...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-core/bff44726-95f0-4e71-8783-a67bfad043ef%40googlegroups.com.

Andrea Leopardi

unread,
Aug 20, 2019, 11:53:42 PM8/20/19
to elixir-l...@googlegroups.com
I’m slightly confused on why we would need to handle nested {:ok, {:ok, _}} tuples. Is that for async_stream specifically?

--
You received this message because you are subscribed to the Google Groups "elixir-lang-core" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-co...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-core/bff44726-95f0-4e71-8783-a67bfad043ef%40googlegroups.com.
--

Andrea Leopardi

Spencer Carlson

unread,
Aug 21, 2019, 12:16:04 AM8/21/19
to elixir-lang-core
Correct.
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-l...@googlegroups.com.
--

Andrea Leopardi

Spencer Carlson

unread,
Aug 21, 2019, 12:24:44 AM8/21/19
to elixir-lang-core
Does it really matter what the structure of the tuple is as long as the first item is either :ok or :error?

I usually start with that exact line, " |> Enum.filter(&match?(&1, {:ok, _}))", but eventually end up building a utility function because it happens so frequently.... my thought was that handling this in a pipe was rote, but if it is not so common, then I agree we should keep it out of the std lib.  
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-l...@googlegroups.com.

Dave Buchanan

unread,
Aug 21, 2019, 8:03:59 PM8/21/19
to elixir-lang-core
I'm a fan.  🙌  I can think of several places I'm going to use this right away!

Ivan Yurov

unread,
Aug 23, 2019, 7:09:06 PM8/23/19
to elixir-lang-core
This is what I created (and am using quite successfully for a while) for this matter: https://hexdocs.pm/monex/MonEx.html#content
It kinda recreates the semantic of Result and Option type from Scala / Rust and others.

W-M Code

unread,
Aug 24, 2019, 4:53:46 PM8/24/19
to elixir-lang-core
In cases where your results are based on a single function returning an ok/error tuple, the size of the tuple will be the same every time, allowing you to use a plain `Enum.filter`, like Chris already mentioned.

In cases where results in your enum are based on various different functions and you might end up having differently-sized tuples in your enumerable, then you should probably refactor this so it becomes normalized again.
Or, if you don't want to, there are a couple of libraries that already allow the functionality that you are looking for.
My own contribution in this regard is the library Solution, which has a Solution.Enum module with functions like `oks`, `combine` (which returns a list of all OK-values, or the first encountered error), etc.
I know that besides Solution and MonEx which was mentioned earlier in this topic, at least the libraries Witchcraft, FunLand, OK, Exceptional, OkJose, HappyWith, ElixirMonad and Towel try to do similar things to make working with ok/error tuples (inside pipes and/or enumerables) easier.

On one hand this does make it seem that maybe there is something there that is so general and ubiquitous that the standard library should adopt it.
On the other (and this is the direction I currently lean towards), it's perfectly fine and nice to have so many different, user-maintained, approaches where for each project the best fit can be chosen.

So no, I don't think we should add this.


~Marten / Qqwy
Reply all
Reply to author
Forward
0 new messages