0

Given:

 data = [[1,2,3, ..., n],[1,2,3, ..., n],[1,2,3, ..., n], ...] 
# List with N rows of equal length

How may we get a row sum: [3,6,9, ..., x]

It is not immediately obvious which of the Enum functions to use, or how to hold a running sum using list comprehension

1 Answer 1

3

I would say, the most readable way would be:

data
|> Enum.zip()
|> Enum.map(fn {v1, v2} -> v1 + v2 end)    
#⇒ [2, 4, 6, ..., x]

For the case of N lists, there is a recursion to be used:

data = [[1,2,3],[1,2,3],[1,2,3]]

defmodule ListSum do
  def mapper([inner1 | [inner2 | rest]]) do
    reduced = inner1
              |> Enum.zip(inner2)
              |> Enum.map(fn {v1, v2} -> v1 + v2 end)
    mapper([reduced | rest])
  end
  def mapper([list]) when is_list(list), do: list
end

IO.inspect ListSum.mapper(data)
#⇒ [3, 6, 9]

The thing is in Erlang/Elixir the easiest approach to extend the solution to a list of inputs is to recursively simplify anything down to a case of a single argument. There are [probably] many ways to rewrite the example above to be better optimized, but I explicitly wrote it the most evident way.


The more evident (yet idiomatically wrong) way for those coming from OO background would be to zip and map tuples to lists:

data
|> Enum.zip()
|> Enum.map(fn e -> e |> Tuple.to_list() |> Enum.reduce(&Kernel.+/2) end)

Benchmarks:

defmodule ListSum do
  def mapper([inner1 | [inner2 | rest]]) do
    reduced = inner1
              |> Enum.zip(inner2)
              |> Enum.map(fn {v1, v2} -> v1 + v2 end)
    mapper([reduced | rest])
  end
  def mapper([list]) when is_list(list), do: list

  def ttler(data) do
    data
    |> Enum.zip()
    |> Enum.map(fn e -> e |> Tuple.to_list() |> Enum.sum() end)
  end
end

defmodule ListSumBench do
  use Benchfella

  @list Enum.to_list(1..1_000)
  @lists List.duplicate(@list, 1_000)

  bench "mapper" do
    ListSum.mapper @lists
  end

  bench "ttler" do
    ListSum.ttler @lists
  end
end

Results:

Compiling 2 files (.ex)
Generated ttl app
Settings:
  duration:      1.0 s

## ListSumBench
[16:49:06] 1/2: mapper
[16:49:09] 2/2: ttler

Finished in 5.56 seconds

## ListSumBench
benchma iterations   average time 
mapper          50   50194.78 µs/op
ttler           10   223662.80 µs/op

The difference between Enum.sum and Enum.reduce(&Kernel.+/2) is insignificant, but sum is a bit faster (like 3%.)

Sign up to request clarification or add additional context in comments.

12 Comments

here's the problem: copying and reusing your code is easy... but understanding why it works, or why this is the obvious way to do it is not so clear
I was trying to write a descriptive “why” but I am a bit out of ideas, what could be a proper wording here. We have two lists of the same length, hence Enum.zip/2 came to my mind. Now we have a single list of tuples, hence Enum.map/2. Probably if you’d state what is a show-stopper in the understanding why is it that way, I could do better in expressing why.
Nope, but I will update the answer for the case of N lists.
Why not just do this for N lists: |> Enum.zip |> Enum.map(fn t -> t |> Tuple.to_list |> Enum.sum end)?
@Dogbert according to the benchmark, the recursion is reproducibly 4.5× faster than Tuple.to_list/1.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.