Skip to content

Commit 7b4a57c

Browse files
authored
Add zip_reduce and zip_reduce_while (#10828)
1 parent 1d7a746 commit 7b4a57c

File tree

2 files changed

+370
-33
lines changed

2 files changed

+370
-33
lines changed

lib/elixir/lib/enum.ex

Lines changed: 235 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -3419,9 +3419,11 @@ defmodule Enum do
34193419
34203420
"""
34213421
@spec zip(t, t) :: [{any, any}]
3422-
def zip(enumerable1, enumerable2)
3423-
when is_list(enumerable1) and is_list(enumerable2) do
3424-
zip_list(enumerable1, enumerable2)
3422+
def zip(enumerable1, enumerable2) when is_list(enumerable1) and is_list(enumerable2) do
3423+
reducer = fn l, r, acc -> {:cont, [{l, r} | acc]} end
3424+
3425+
zip_reduce_while(enumerable1, enumerable2, [], reducer)
3426+
|> :lists.reverse()
34253427
end
34263428

34273429
def zip(enumerable1, enumerable2) do
@@ -3448,20 +3450,35 @@ defmodule Enum do
34483450
def zip([]), do: []
34493451

34503452
def zip(enumerables) do
3451-
Stream.zip(enumerables).({:cont, []}, &{:cont, [&1 | &2]})
3452-
|> elem(1)
3453+
zip_reduce_while(enumerables, [], &{:cont, [List.to_tuple(&1) | &2]})
34533454
|> :lists.reverse()
34543455
end
34553456

34563457
@doc """
34573458
Zips corresponding elements from two enumerables into a list, transforming them with
34583459
the `zip_fun` function as it goes.
34593460
3460-
The corresponding elements from each collection are passed to the provided 2-arity `zip_fun` function in turn.
3461-
Returns a list that contains the result of calling `zip_fun` for each pair of elements.
3461+
The corresponding elements from each collection are passed to the provided 2-arity `zip_fun`
3462+
function in turn. Returns a list that contains the result of calling `zip_fun` for each pair of
3463+
elements.
34623464
34633465
The zipping finishes as soon as either enumerable runs out of elements.
34643466
3467+
## Zipping Maps
3468+
3469+
It's important to remember that zipping inherently relies on order. If you zip two lists you get
3470+
the element at the index from each list in turn. If we zip two maps together it's tempting to
3471+
think that you will get the given key in the left map and the matching key in the right map, but
3472+
there is no such guarantee because map keys are not ordered! Consider the following:
3473+
3474+
left = %{:a => 1, 1 => 3}
3475+
right = %{:a => 1, :b => :c}
3476+
Enum.zip(left, right)
3477+
# [{{1, 3}, {:a, 1}}, {{:a, 1}, {:b, :c}}]
3478+
3479+
As you can see `:a` does not get paired with `:a`. If this is what you want, you should use
3480+
`Map.merge/3`
3481+
34653482
## Examples
34663483
34673484
iex> Enum.zip_with([1, 2], [3, 4], fn x, y -> x + y end)
@@ -3476,28 +3493,39 @@ defmodule Enum do
34763493
"""
34773494
@doc since: "1.12.0"
34783495
@spec zip_with(t, t, (enumerable1_elem :: term, enumerable2_elem :: term -> term)) :: [term]
3479-
def zip_with(enumerable1, enumerable2, zip_fun)
3480-
when is_list(enumerable1) and is_list(enumerable2) and is_function(zip_fun, 2) do
3481-
zip_list(enumerable1, enumerable2, zip_fun)
3482-
end
3483-
34843496
def zip_with(enumerable1, enumerable2, zip_fun) when is_function(zip_fun, 2) do
3485-
# zip_with/2 passes a list to the zip_fun containing the nth element from each enumerable
3486-
# That's different from zip_with/3 where each element is a different argument to the zip_fun
3487-
# apply/2 ensures that zip_fun gets the right number of arguments.
3488-
zip_with([enumerable1, enumerable2], &apply(zip_fun, &1))
3497+
reducer = fn l, r, acc -> {:cont, [zip_fun.(l, r) | acc]} end
3498+
3499+
zip_reduce_while(enumerable1, enumerable2, [], reducer)
3500+
|> :lists.reverse()
34893501
end
34903502

34913503
@doc """
34923504
Zips corresponding elements from a finite collection of enumerables into list, transforming them with
34933505
the `zip_fun` function as it goes.
34943506
3495-
The first element from each of the enums in `enumerables` will be put into a list which is then passed to
3496-
the 1-arity `zip_fun` function. Then, the second elements from each of the enums are put into a list and passed to
3497-
`zip_fun`, and so on until any one of the enums in `enumerables` runs out of elements.
3507+
The first element from each of the enums in `enumerables` will be put into a list which is then
3508+
passed to the 1-arity `zip_fun` function. Then, the second elements from each of the enums are
3509+
put into a list and passed to `zip_fun`, and so on until any one of the enums in `enumerables`
3510+
runs out of elements.
34983511
34993512
Returns a list with all the results of calling `zip_fun`.
35003513
3514+
## Zipping Maps
3515+
3516+
It's important to remember that zipping inherently relies on order. If you zip two lists you get
3517+
the element at the index from each list in turn. If we zip two maps together it's tempting to
3518+
think that you will get the given key in the left map and the matching key in the right map, but
3519+
there is no such guarantee because map keys are not ordered! Consider the following:
3520+
3521+
left = %{:a => 1, 1 => 3}
3522+
right = %{:a => 1, :b => :c}
3523+
Enum.zip(left, right)
3524+
# [{{1, 3}, {:a, 1}}, {{:a, 1}, {:b, :c}}]
3525+
3526+
As you can see `:a` does not get paired with `:a`. If this is what you want, you should use
3527+
`Map.merge/3`
3528+
35013529
## Examples
35023530
35033531
iex> Enum.zip_with([[1, 2], [3, 4], [5, 6]], fn [x, y, z] -> x + y + z end)
@@ -3512,11 +3540,197 @@ defmodule Enum do
35123540
def zip_with([], _fun), do: []
35133541

35143542
def zip_with(enumerables, zip_fun) do
3515-
Stream.zip_with(enumerables, zip_fun).({:cont, []}, &{:cont, [&1 | &2]})
3516-
|> elem(1)
3543+
reducer = fn values, acc -> {:cont, [zip_fun.(values) | acc]} end
3544+
3545+
zip_reduce_while(enumerables, [], reducer)
35173546
|> :lists.reverse()
35183547
end
35193548

3549+
@doc """
3550+
Reduces a over two enumerables halting as soon as either enumerable is empty.
3551+
3552+
## Zipping Maps
3553+
3554+
It's important to remember that zipping inherently relies on order. If you zip two lists you get
3555+
the element at the index from each list in turn. If we zip two maps together it's tempting to
3556+
think that you will get the given key in the left map and the matching key in the right map, but
3557+
there is no such guarantee because map keys are not ordered! Consider the following:
3558+
3559+
left = %{:a => 1, 1 => 3}
3560+
right = %{:a => 1, :b => :c}
3561+
Enum.zip(left, right)
3562+
# [{{1, 3}, {:a, 1}}, {{:a, 1}, {:b, :c}}]
3563+
3564+
As you can see `:a` does not get paired with `:a`. If this is what you want, you should use
3565+
`Map.merge/3`
3566+
3567+
## Examples
3568+
3569+
iex> Enum.zip_reduce([1, 2], [3, 4], 0, fn x, y, acc -> x + y + acc end)
3570+
10
3571+
3572+
iex> Enum.zip_reduce([1, 2], [3, 4], [], fn x, y, acc -> [x + y |acc] end)
3573+
[6, 4]
3574+
"""
3575+
def zip_reduce(left, right, acc, reducer) do
3576+
non_stop_reducer = &{:cont, reducer.(&1, &2, &3)}
3577+
zip_reduce_while(left, right, acc, non_stop_reducer)
3578+
end
3579+
3580+
@doc """
3581+
Reduces a over all of the given enums, halting as soon as any enumerable is empty.
3582+
3583+
The reducer will receive 2 args, a list of elements (one from each enum) and the
3584+
accumulator.
3585+
3586+
## Zipping Maps
3587+
3588+
It's important to remember that zipping inherently relies on order. If you zip two lists you get
3589+
the element at the index from each list in turn. If we zip two maps together it's tempting to
3590+
think that you will get the given key in the left map and the matching key in the right map, but
3591+
there is no such guarantee because map keys are not ordered! Consider the following:
3592+
3593+
left = %{:a => 1, 1 => 3}
3594+
right = %{:a => 1, :b => :c}
3595+
Enum.zip(left, right)
3596+
# [{{1, 3}, {:a, 1}}, {{:a, 1}, {:b, :c}}]
3597+
3598+
As you can see `:a` does not get paired with `:a`. If this is what you want, you should use
3599+
`Map.merge/3`
3600+
3601+
## Examples
3602+
3603+
iex> enums = [[1, 1], [2, 2], [3, 3]]
3604+
...> Enum.zip_reduce(enums, [], fn elements, acc ->
3605+
...> [List.to_tuple(elements) | acc]
3606+
...> end)
3607+
[{1, 2, 3}, {1, 2, 3}]
3608+
3609+
iex> enums = [[1, 2], %{a: 3, b: 4}, [5, 6]]
3610+
...> Enum.zip_reduce(enums, [], fn elements, acc ->
3611+
...> [List.to_tuple(elements) | acc]
3612+
...> end)
3613+
[{2, {:b, 4}, 6}, {1, {:a, 3}, 5}]
3614+
"""
3615+
def zip_reduce(enums, acc, reducer) do
3616+
non_stop_reducer = &{:cont, reducer.(&1, &2)}
3617+
zip_reduce_while(enums, acc, non_stop_reducer)
3618+
end
3619+
3620+
@doc """
3621+
Reduces over two enumerables halting if the accumulator returns `{:halt, value}` or if
3622+
either of the enumerables is empty.
3623+
3624+
The reducer will receive 3 args, the left enumerable's element, the right enumberable's
3625+
element and the accumulator. It should return one of:
3626+
3627+
* `{:halt, value}` - This will halt the reduction and return `value`
3628+
* `{:cont, value}` - This will continue with the next step of the reduction.
3629+
3630+
## Zipping Maps
3631+
3632+
It's important to remember that zipping inherently relies on order. If you zip two lists you get
3633+
the element at the index from each list in turn. If we zip two maps together it's tempting to
3634+
think that you will get the given key in the left map and the matching key in the right map, but
3635+
there is no such guarantee because map keys are not ordered! Consider the following:
3636+
3637+
left = %{:a => 1, 1 => 3}
3638+
right = %{:a => 1, :b => :c}
3639+
Enum.zip(left, right)
3640+
# [{{1, 3}, {:a, 1}}, {{:a, 1}, {:b, :c}}]
3641+
3642+
As you can see `:a` does not get paired with `:a`. If this is what you want, you should use
3643+
`Map.merge/3`
3644+
3645+
## Examples
3646+
3647+
iex> reducer = fn left, right, acc -> {:cont, [left + right | acc ]} end
3648+
...> Enum.zip_reduce_while([1], [2], [], reducer)
3649+
[3]
3650+
3651+
iex> Enum.zip_reduce_while([1, 2], [2, 2], [], fn left, right, acc ->
3652+
...> if left <= 1, do: {:cont, [left + right | acc]}, else: {:halt, acc}
3653+
...> end)
3654+
[3]
3655+
3656+
iex> left = [1, 2]
3657+
...> right = [3, 4]
3658+
...> Enum.zip_reduce_while(left, right, [], fn l, r, acc ->
3659+
...> {:suspend, [l + r | acc]}
3660+
...> end)
3661+
[4]
3662+
"""
3663+
def zip_reduce_while(left, right, acc, reducer) when is_list(left) and is_list(right) do
3664+
zip_reduce_while_list(left, right, {:cont, acc}, reducer) |> elem(1)
3665+
end
3666+
3667+
def zip_reduce_while(left, right, acc, reducer) do
3668+
reduce = fn [l, r], acc -> reducer.(l, r, acc) end
3669+
Stream.zip_with([left, right], & &1).({:cont, acc}, reduce) |> elem(1)
3670+
end
3671+
3672+
@doc """
3673+
Reduces over all enumerables halting if the accumulator returns `{:halt, value}` or if
3674+
any of the enumerables is empty.
3675+
3676+
The reducer will receive 2 args, a list of the yielded elements and the accumulator.
3677+
It should return one of:
3678+
3679+
* `{:halt, value}` - This will halt the reduction and return `value`
3680+
* `{:cont, value}` - This will continue with the next step of the reduction.
3681+
3682+
## Zipping Maps
3683+
3684+
It's important to remember that zipping inherently relies on order. If you zip two lists you get
3685+
the element at the index from each list in turn. If we zip two maps together it's tempting to
3686+
think that you will get the given key in the left map and the matching key in the right map, but
3687+
there is no such guarantee because map keys are not ordered! Consider the following:
3688+
3689+
left = %{:a => 1, 1 => 3}
3690+
right = %{:a => 1, :b => :c}
3691+
Enum.zip(left, right)
3692+
# [{{1, 3}, {:a, 1}}, {{:a, 1}, {:b, :c}}]
3693+
3694+
As you can see `:a` does not get paired with `:a`. If this is what you want, you should use
3695+
`Map.merge/3`
3696+
3697+
## Examples
3698+
3699+
iex> enums = [[1, 2],[3, 4]]
3700+
...> reducer = fn values, acc -> {:cont, Enum.sum(values) + acc} end
3701+
...> Enum.zip_reduce_while(enums, 0, reducer)
3702+
10
3703+
3704+
iex> enums = [[1, 2],[3, 4]]
3705+
...> reducer = fn values, acc -> {:suspend, [Enum.sum(values) | acc]} end
3706+
...> Enum.zip_reduce_while(enums, [], reducer)
3707+
[4]
3708+
3709+
iex> enums = [[1, 2],[3, 4]]
3710+
...> reducer = fn values, acc -> {:halt, [Enum.sum(values) | acc]} end
3711+
...> Enum.zip_reduce_while(enums, [], reducer)
3712+
[4]
3713+
"""
3714+
def zip_reduce_while([], acc, _reducer), do: acc
3715+
3716+
def zip_reduce_while(enums, acc, reducer) do
3717+
Stream.zip_with(enums, & &1).({:cont, acc}, reducer) |> elem(1)
3718+
end
3719+
3720+
# This speeds things up when zip reducing two lists.
3721+
defp zip_reduce_while_list(_left, _right, {:halt, acc}, _), do: {:halted, acc}
3722+
3723+
defp zip_reduce_while_list(left, right, {:suspend, acc}, reducer) do
3724+
{:suspended, acc, &zip_reduce_while_list(left, right, &1, reducer)}
3725+
end
3726+
3727+
defp zip_reduce_while_list([], _right, {:cont, acc}, _), do: {:done, acc}
3728+
defp zip_reduce_while_list(_left, [], {:cont, acc}, _), do: {:done, acc}
3729+
3730+
defp zip_reduce_while_list([l_head | l_tail], [r_head | r_tail], {:cont, acc}, reducer) do
3731+
zip_reduce_while_list(l_tail, r_tail, reducer.(l_head, r_head, acc), reducer)
3732+
end
3733+
35203734
## Helpers
35213735

35223736
@compile {:inline, entry_to_string: 1, reduce: 3, reduce_by: 3, reduce_enumerable: 3}
@@ -4118,18 +4332,6 @@ defmodule Enum do
41184332
defp uniq_list([], _set, _fun) do
41194333
[]
41204334
end
4121-
4122-
## zip
4123-
defp zip_list(enumerable1, enumerable2) do
4124-
zip_list(enumerable1, enumerable2, fn x, y -> {x, y} end)
4125-
end
4126-
4127-
defp zip_list([head1 | next1], [head2 | next2], fun) do
4128-
[fun.(head1, head2) | zip_list(next1, next2, fun)]
4129-
end
4130-
4131-
defp zip_list(_, [], _fun), do: []
4132-
defp zip_list([], _, _fun), do: []
41334335
end
41344336

41354337
defimpl Enumerable, for: List do

0 commit comments

Comments
 (0)