META
Number: ELL 2
Title: Implement Enum.zip_map
Contributors: Quentin Crain, bjfish, elixir-lang slackers
TL;DR
Abstract:
zip two enumerables’ elements into a tuple, padding the shorter with :nil (default), and map to your function; ie. zip and map in one pass. We have filter_map, so why not this?! :)
Implementation:
def zip_map(list1, list2, func, padding \\ :nil) do
l1 = length(list1)
l2 = length(list2)
if l1 < l2 do
list1 = list1
++
(Stream.cycle([padding]) |> Stream.take(l2-l1) |> Enum.to_list)
else
list2 = list2
++
(Stream.cycle([padding]) |> Stream.take(l1-l2) |> Enum.to_list)
end
Enum.zip(list1, list2)
|> Enum.map(func)
end
Examples:
# Concat two lists of lists
a_thru_m = (for e <- 97..109, do: [e])
['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm']
n_thru_z = (for e <- 110..122, do: [e])
['n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']
zip_map(a_thru_m, n_thru_z, fn ({x,y}) -> x++y end)
['an', 'bo', 'cp', 'dq', 'er', 'fs', 'gt', 'hu', 'iv', 'jw', 'kx', 'ly', 'mz']
# Sum the elements of two lists
one_thru_ten = 1..10 |> Enum.to_list
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
one_thru_five = 1..5 |> Enum.to_list
[1, 2, 3, 4, 5]
zip_map(one_thru_ten, one_thru_five, fn ({x,y}) -> x+y end, 0)
[2, 4, 6, 8, 10, 6, 7, 8, 9, 10]
STORY
I’m old. I’m a GenX-er. I press ⌘-+ at least twice on webpages nowadays. I have a daughter in college, damn it!
Oh great, another geezer story. Geezers love narratives, especially their own!
So, I figured it was time to get off my ass; I knew I needed to get out of my cubicle, at least virtually. I had been working in Python for 15 years and wasn’t growing. I had just designed a test framework as microservices, but wasn’t happy with a number of things. So, I looked around and of course found Erlang/OTP. Then found Elixir. That was all of 3months ago.
Dropped Python cold. Didn’t have to, but no better way to learn eh? Like “real” languages.
So, how to learn? Best way for me: read the group’s interactive communication channels. This meant elixir-lang-talk, elixir-lang-core and elixir-lang on slack.
Ok, ok, hurrying the story along: bjfish on slack wanted to split a list into N number of lists, like this:
distribute([1, 2, 3, 4, 5], 3)
[[1, 4], [2, 5], [3]] (or [[1, 2], [3, 4], [5]], he didn’t really care)
He implemented it by grouping on the div of each element’s index (assuming I understand his solution correctly!) Most of the replies, though, wanted to use Enum.chunk and so did I. So I spent 3 days on it.
Obsessive and stupid, I know!
What seems like the right solution is to be able to Enum.chunk as many elements as possible and then distribute the remaining elements over the chunks. All very easy:
{chunkable, remaining} = Enum.split(SOME-LIST, SOME-MATH-TO-PARTITION)
chunkable = Enum.chunk(chunkable, CHUNK-SIZE)
Of course at this point I just wanted to zip the lists and then map them by appending/++ each element of remaining to a chunk in chunkable. But they are of different lengths so good ‘ol zip wasn’t going to help me. I did it with reduce but thought it ugly.
What I wanted -- nay needed! -- was zip_map! So here it is hopefully making the solution to bjfish’s need very clear:
zip_map(chunkable, remaining,
fn
({element_from_chunkable, :nil}) ->
element_from_chunkable
({element_from_chunkable, element_from_remaining}) ->
element_from_chunkable ++ [element_from_remaining]
end
)
That’s why I’m propose zip_map. Thoughts?
There are two proposals within this one.
First, a form of zip that pads rather than "finishes as soon as any enumerable completes."
Second, a convenient zip_map function.
For a padding zip, I think Enum.pad/2 and Stream.pad/2 are possibilities, though like your implementation it wouldn't work for indeterminate or infinite streams. The real question isn't "How do I make ListA have length N?" It's "How do I make ListA have the same length as ListB?"
That's not really something easily solved in with the standard Enum/Stream funs, as there's a coupling between the two the two Lists. One solution that would work with indefinite streams:
ref = make_ref
padding = Stream.cycle([ref])
Stream.zip(Stream.concat(list1, padding), Stream.concat(list2, padding))
|> Stream.take_while(&({ref,ref} != &1))
Or something like that. That's still too complex to call it an "idiomatic solution" (plus perf issues). So I think padded zip still has a place (it would need a cleaner implementation though).
As for zip_map, I'm not a huge fan when Stream.zip |> Enum.map would accomplish the same thing.
Also, Welcome!
- Peter
--
You received this message because you are subscribed to the Google Groups "elixir-lang-talk" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-ta...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-talk/e1565486-c76f-4582-837c-ba4f734c3ae7%40googlegroups.com.
For more options, visit https://groups.google.com/d/optout.
--
You received this message because you are subscribed to the Google Groups "elixir-lang-talk" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-ta...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-talk/cbb3fb7d-e199-4790-a9f0-a0b2b70cdb6e%40googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-talk/CAOMhEnyEcEAeqGEbieY2EfRYWZgY1HdE5xkiYPaWNgbbW5BCwQ%40mail.gmail.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-talk/CAGnRm4LQcpZ7sadW5%3DtV_GFAVKabwjj3_c2rV_JMi%2BYpvJ3cFg%40mail.gmail.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-talk/CAOMhEnwwq%2Bw9nBCY7atiPX2Yd%3DTzSTz6_BVaGcYjZyrK9a2X5g%40mail.gmail.com.
For more options, visit https://groups.google.com/d/optout.
Maybe we should support options like :pad and :cycle? Not sure I like it though.
On Tuesday, April 12, 2016, Peter Hamilton <> wrote:
From a usability standpoint, I don't find it too cumbersome to do:Enum.chunk(list, 15, 15, Stream.cycle([nil]))There may be a performance hit though.If we make zip/3 cycle by default, then there isn't a possibility of doing the normal pad (whereas the opposite would work).I'm also intrigued (not sure if I like it or dislike it) by the idea of padding with a Stream full of side effects, which wouldn't work if we only allowed padding with a cycle.
On Tue, Apr 12, 2016 at 10:49 AM José Valim <> wrote:
I thought about chunk/4 as well but do we want the same semantics? In chunk/4, the padding is used as is. If the pad is too short, the final chunk is too short. That's ok for chunk/4 because the chunk size is known before hand. That's not the case for zip and I think we would rather prefer to cycle over the pad (for example, if I give [1, 2, 3], it will complete it with [1, 2, 3, 1, 2, 3, ...]). I am not settled on the name, let's figure out the semantics and we can move to the name next. Thoughts?
--
You received this message because you are subscribed to the Google Groups "elixir-lang-talk" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-ta...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-talk/a929accc-223d-4154-bd27-cb6f7aed1b10%40googlegroups.com.
Yes. With the case:zip(1..8, 1..5, Stream.cycle(["Marcia"]))[{1, 1}, {2, 2}, {3, 3}, {4, 4}, {5, 5}, {6, "Marcia"}, {7, "Marcia"}, {8, "Marcia"}]also being true (important that the pad goes to whichever stream ends first)
--
You received this message because you are subscribed to the Google Groups "elixir-lang-talk" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-ta...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-talk/CALNYqAsQs%2Bjg9JCV3c5a%3DtR3uwdQ8rtj%3DM3wb%3D-%3DNuDOUOEg4Q%40mail.gmail.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-talk/CAOMhEnx5840q6z5sYzB8BtADrEhpWgFV852ToQ6maQu07%2B%3Dm6w%40mail.gmail.com.
For more options, visit https://groups.google.com/d/optout.
To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-talk/CAGnRm4LOxCeCbWhKeLJS_cEbp_iPyzTdNHRaWMQrMweATBh0FA%40mail.gmail.com.
--
You received this message because you are subscribed to the Google Groups "elixir-lang-talk" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-ta...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-talk/fd5c5d0d-0c11-45f8-9d57-55f8c6bd070e%40googlegroups.com.