dimitarvp

dimitarvp

Can you improve this? Zipping two lists, the result must be same size as the first list

Heya.
I am inviting you to copy-paste the module below and add your own functions and measure them, or simply give other ideas about how the desired result can be produced best.

I got interested in writing a function that takes two lists where the first is always bigger than the second and make pairs a la Enum.zip but not stop when the smaller list depletes. I want it to continue, while cycling through the second list, until we have a list with the same size as the first one, containing 2-size tuples.

Example:

list0 = [1, 2, 3, 4, 5]
list1 = [:a, :b]

I want the output to be:

[{1, :a}, {2, :b}, {3, :a}, {4, :b}, {5, :a}]

I came up with these two functions (and benchmark code):

defmodule Xyz do
  def pair_with_enum_reduce(list0, list1) do
    list1_tuple = List.to_tuple(list1)
    list1_last_position = tuple_size(list1_tuple) - 1

    Enum.reduce(list0, {[], 0}, fn item, {final_list, position} ->
      mapped_item = {item, elem(list1_tuple, position)}

      position =
        case position do
          ^list1_last_position ->
            0

          _ ->
            position + 1
        end

      {[mapped_item | final_list], position}
    end)
    |> elem(0)
    |> Enum.reverse()
  end

  def pair_with_stream_cycle(list0, list1) do
    stream = Stream.cycle(list1)
    Enum.zip(list0, Enum.take(stream, length(list0)))
  end

  def bench() do
    list0 = Enum.to_list(1..2000)
    list1 = [:worker_0, :worker_1, :worker_2, :worker_3, :worker_4]

    Benchee.run(%{
      "pair_with_enum_reduce" => fn -> pair_with_enum_reduce(list0, list1) end,
      "pair_with_stream_cycle" => fn -> pair_with_stream_cycle(list0, list1) end
    })
  end
end

Benchmark results on my machine:

Name                             ips        average  deviation         median         99th %
pair_with_enum_reduce        19.80 K       50.49 μs    ±28.32%          50 μs          85 μs
pair_with_stream_cycle        7.64 K      130.89 μs    ±40.15%         108 μs      327.12 μs

Comparison:
pair_with_enum_reduce        19.80 K
pair_with_stream_cycle        7.64 K - 2.59x slower +80.39 μs

I know Stream incurs some performance penalty but was rather surprised by how much. I don’t like the size of the pair_with_enum_reduce function, nor the fact that it has to call Enum.reverse at the end but it’s still significantly faster. And I don’t like that the pair_with_stream_cycle relies on calling length on the first list. Both functions I am kind of unhappy with.

Any criticisms? And, do you think you could do better?

(Alternatively, another implementation might not care about which size list is bigger.)

Marked As Solved

LostKobrakai

LostKobrakai

That’s what I created as well, though slightly different. First using Stream.unfold, then using tail recursion. The latter being about twice as fast.

Summary
def pair_with_unfold(list0, list1) do
    Stream.unfold({list0, list1, list1}, fn 
     {[], _, _list1} -> nil
     {[head_l0 | rest_l0], [], [head_l1 | rest_l1] = list1} -> {{head_l0, head_l1}, {rest_l0, rest_l1, list1}}
     {[head_l0 | rest_l0], [head_l1 | rest_l1], list1} -> {{head_l0, head_l1}, {rest_l0, rest_l1, list1}}
    end)
    |> Enum.to_list()
  end

  def pair_with_recursion(list0, list1) do
    pair_with_recursion(list0, list1, list1, [])
  end

  defp pair_with_recursion([], _, _list1, acc) do
    Enum.reverse(acc)
  end

  defp pair_with_recursion([head_l0 | rest_l0], [], [head_l1 | rest_l1] = list1, acc) do
    pair_with_recursion(rest_l0, rest_l1, list1, [{head_l0, head_l1} | acc])
  end

  defp pair_with_recursion([head_l0 | rest_l0], [head_l1 | rest_l1], list1, acc) do
    pair_with_recursion(rest_l0, rest_l1, list1, [{head_l0, head_l1} | acc])
  end

Also Liked

dimitarvp

dimitarvp

Yes, absolutely. Not all threads should be about the 2763th person panicking when iex shows them two characters when trying to print e.g. [10, 13]. :slightly_frowning_face: Or “how do I make this Ecto query” or “how do I do X with Phoenix”. Or “I haven’t read even the basics of Elixir but please write this code for me”… :021:

Threads like this one give people a chance to go deep and work on a small but (hopefully) interesting problem and show their talents. We indeed need more of them and I hope others will not be shy and start such threads.

BartOtten

BartOtten

@dimitarvp This topic is awesome!

I would like to see more of those ‘using the community to find the best solution’-topics from experienced Elixir developers (opposed to newbie questions). Great value and with a good amount of SEO it might be so that inexperienced developers find those topics first :wink:

derek-zhou

derek-zhou

How about this:

  def pair_with_body_recursion(list0, list1) do                                                     
    pair_recursion(list0, list1, list1)                                                             
  end                                                                                               
                                                                                                    
  defp pair_recursion([], _, _), do: []                                                             
  defp pair_recursion(l0, [], l1), do: pair_recursion(l0, l1, l1)                                   
  defp pair_recursion([h0 | t0], [h1 | t1], l1) do                                                  
    [{h0, h1} | pair_recursion(t0, t1, l1)]                                                         
  end                                                                                               

Last Post!

suprafly

suprafly

I didn’t benchmark at all, but the game seemed fun so throwing a solution,

Enum.with_index(list0) |> Enum.map(fn {el, i} -> {el, Enum.at(list1, rem(i, length(list1)))} end)

Where Next?

Popular in Questions Top

vegabook
I’m brand new to Phoenix and I have stripped one of the demo applications to the bone. I just want to get an svg up on the screen. Here i...
New
9mm
I am constructing a JSON object (map) and I need to conditionally set a field. I’m trying to write proper elixir-way code… and I’m at a l...
New
komlanvi
Hi everyone, I was playing with phoenix liveView but I run into an issue. I have a form and want to validate each input text when the te...
New
aalberti333
As the title describes, I’m trying to run Enum.map() over a list of key/value pairs, where the value is a map. My data looks like this: ...
New
siddhant3030
Hi, I have to write a raw query for one of my project. But till now I have used ecto queries and don’t have much experience writing raw ...
New
freewebwithme
Using vs code and installed ElixirLS: support and debugger. And I got an error popped up on start up says Failed to run ‘elixir’ comma...
New
Patoshizzle
After calling mix ecto.create I get this error: 17:00:32.162 [error] GenServer #PID<0.412.0> terminating ** (Postgrex.Error) FATAL...
New

Other popular topics Top

rms.mrcs
Hi, I need to transform a list of numbers into a map where the keys are the indexes and the values are the original values of the list. ...
New
grych
Hi folks, Few months ago I have announced the proof-of-concept of the library to manipulate the browsers DOM objects directly from Elixi...
639 54006 488
New
vonH
When I run the Plug and I recompile I wind up having to use Ctrl C to quit iex and start again. Witht the help of rlwrap I can use the cu...
New
vonH
In asking this question I am more interested about the expressiveness of the language itself and less concerned about the availability of...
New
gshaw
What is the idiomatic way of matching for not nil in Elixir? E.g., First way: defp halt_if_not_signed_in(conn, signed_in_account) when...
New
sen
Hi All, I set a environment variables in dev.exs , like below code. when i start server, how can i set the ${enable} value? thanks. d...
New

We're in Beta

About us Mission Statement