Skip to content

Commit de8e4f1

Browse files
committed
Optimize Stream.zip and Stream.zip_with with only lists
1 parent cd18fc7 commit de8e4f1

File tree

1 file changed

+87
-36
lines changed

1 file changed

+87
-36
lines changed

lib/elixir/lib/stream.ex

Lines changed: 87 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1119,7 +1119,27 @@ defmodule Stream do
11191119
"""
11201120
@spec zip(Enumerable.t(), Enumerable.t()) :: Enumerable.t()
11211121
def zip(enumerable1, enumerable2) do
1122-
zip([enumerable1, enumerable2])
1122+
zip_with(enumerable1, enumerable2, fn left, right -> {left, right} end)
1123+
end
1124+
1125+
@doc """
1126+
Zips corresponding elements from a finite collection of enumerables
1127+
into one stream of tuples.
1128+
1129+
The zipping finishes as soon as any enumerable in the given collection completes.
1130+
1131+
## Examples
1132+
1133+
iex> concat = Stream.concat(1..3, 4..6)
1134+
iex> cycle = Stream.cycle(["foo", "bar", "baz"])
1135+
iex> Stream.zip([concat, [:a, :b, :c], cycle]) |> Enum.to_list()
1136+
[{1, :a, "foo"}, {2, :b, "bar"}, {3, :c, "baz"}]
1137+
1138+
"""
1139+
@doc since: "1.4.0"
1140+
@spec zip(enumerables) :: Enumerable.t() when enumerables: [Enumerable.t()] | Enumerable.t()
1141+
def zip(enumerables) do
1142+
zip_with(enumerables, &List.to_tuple(&1))
11231143
end
11241144

11251145
@doc """
@@ -1139,28 +1159,28 @@ defmodule Stream do
11391159
"""
11401160
@doc since: "1.12.0"
11411161
@spec zip_with(Enumerable.t(), Enumerable.t(), (term, term -> term)) :: Enumerable.t()
1142-
def zip_with(enumerable1, enumerable2, zip_fun) when is_function(zip_fun, 2) do
1143-
zip_with([enumerable1, enumerable2], &apply(zip_fun, &1))
1162+
def zip_with(enumerable1, enumerable2, zip_fun)
1163+
when is_list(enumerable1) and is_list(enumerable2) and is_function(zip_fun, 2) do
1164+
&zip_pair(enumerable1, enumerable2, &1, &2, zip_fun)
11441165
end
11451166

1146-
@doc """
1147-
Zips corresponding elements from a finite collection of enumerables
1148-
into one stream of tuples.
1167+
def zip_with(enumerable1, enumerable2, zip_fun) when is_function(zip_fun, 2) do
1168+
zip_with([enumerable1, enumerable2], fn [left, right] -> zip_fun.(left, right) end)
1169+
end
11491170

1150-
The zipping finishes as soon as any enumerable in the given collection completes.
1171+
defp zip_pair(_list1, _list2, {:halt, acc}, _fun, _zip_fun) do
1172+
{:halted, acc}
1173+
end
11511174

1152-
## Examples
1175+
defp zip_pair(list1, list2, {:suspend, acc}, fun, zip_fun) do
1176+
{:suspended, acc, &zip_pair(list1, list2, &1, fun, zip_fun)}
1177+
end
11531178

1154-
iex> concat = Stream.concat(1..3, 4..6)
1155-
iex> cycle = Stream.cycle(["foo", "bar", "baz"])
1156-
iex> Stream.zip([concat, [:a, :b, :c], cycle]) |> Enum.to_list()
1157-
[{1, :a, "foo"}, {2, :b, "bar"}, {3, :c, "baz"}]
1179+
defp zip_pair([], _list2, {:cont, acc}, _fun, _zip_fun), do: {:done, acc}
1180+
defp zip_pair(_list1, [], {:cont, acc}, _fun, _zip_fun), do: {:done, acc}
11581181

1159-
"""
1160-
@doc since: "1.4.0"
1161-
@spec zip(enumerables) :: Enumerable.t() when enumerables: [Enumerable.t()] | Enumerable.t()
1162-
def zip(enumerables) do
1163-
zip_with(enumerables, &List.to_tuple(&1))
1182+
defp zip_pair([head1 | tail1], [head2 | tail2], {:cont, acc}, fun, zip_fun) do
1183+
zip_pair(tail1, tail2, fun.(zip_fun.(head1, head2), acc), fun, zip_fun)
11641184
end
11651185

11661186
@doc """
@@ -1188,10 +1208,41 @@ defmodule Stream do
11881208
@spec zip_with(enumerables, (Enumerable.t() -> term)) :: Enumerable.t()
11891209
when enumerables: [Enumerable.t()] | Enumerable.t()
11901210
def zip_with(enumerables, zip_fun) when is_function(zip_fun, 1) do
1191-
&prepare_zip(enumerables, &1, &2, zip_fun)
1211+
if is_list(enumerables) and :lists.all(&is_list/1, enumerables) do
1212+
&zip_list(enumerables, &1, &2, zip_fun)
1213+
else
1214+
&zip_enum(enumerables, &1, &2, zip_fun)
1215+
end
1216+
end
1217+
1218+
defp zip_list(_enumerables, {:halt, acc}, _fun, _zip_fun) do
1219+
{:halted, acc}
1220+
end
1221+
1222+
defp zip_list(enumerables, {:suspend, acc}, fun, zip_fun) do
1223+
{:suspended, acc, &zip_list(enumerables, &1, fun, zip_fun)}
1224+
end
1225+
1226+
defp zip_list(enumerables, {:cont, acc}, fun, zip_fun) do
1227+
case zip_list_heads_tails(enumerables, [], []) do
1228+
{heads, tails} -> zip_list(tails, fun.(zip_fun.(heads), acc), fun, zip_fun)
1229+
:error -> {:done, acc}
1230+
end
1231+
end
1232+
1233+
defp zip_list_heads_tails([[head | tail] | rest], heads, tails) do
1234+
zip_list_heads_tails(rest, [head | heads], [tail | tails])
1235+
end
1236+
1237+
defp zip_list_heads_tails([[] | _rest], _heads, _tails) do
1238+
:error
1239+
end
1240+
1241+
defp zip_list_heads_tails([], heads, tails) do
1242+
{:lists.reverse(heads), :lists.reverse(tails)}
11921243
end
11931244

1194-
defp prepare_zip(enumerables, acc, fun, zip_fun) do
1245+
defp zip_enum(enumerables, acc, fun, zip_fun) do
11951246
step = fn x, acc ->
11961247
{:suspend, :lists.reverse([x | acc])}
11971248
end
@@ -1201,42 +1252,42 @@ defmodule Stream do
12011252
{&Enumerable.reduce(enum, &1, step), [], :cont}
12021253
end)
12031254

1204-
do_zip(enum_funs, acc, fun, zip_fun)
1255+
do_zip_enum(enum_funs, acc, fun, zip_fun)
12051256
end
12061257

1207-
# This implementation of do_zip/4 works for any number of streams to zip
1208-
defp do_zip(zips, {:halt, acc}, _fun, _zip_fun) do
1258+
# This implementation of do_zip_enum/4 works for any number of streams to zip
1259+
defp do_zip_enum(zips, {:halt, acc}, _fun, _zip_fun) do
12091260
do_zip_close(zips)
12101261
{:halted, acc}
12111262
end
12121263

1213-
defp do_zip(zips, {:suspend, acc}, fun, zip_fun) do
1214-
{:suspended, acc, &do_zip(zips, &1, fun, zip_fun)}
1264+
defp do_zip_enum(zips, {:suspend, acc}, fun, zip_fun) do
1265+
{:suspended, acc, &do_zip_enum(zips, &1, fun, zip_fun)}
12151266
end
12161267

1217-
defp do_zip([], {:cont, acc}, _callback, _zip_fun) do
1268+
defp do_zip_enum([], {:cont, acc}, _callback, _zip_fun) do
12181269
{:done, acc}
12191270
end
12201271

1221-
defp do_zip(zips, {:cont, acc}, callback, zip_fun) do
1272+
defp do_zip_enum(zips, {:cont, acc}, callback, zip_fun) do
12221273
try do
1223-
do_zip_next_tuple(zips, acc, callback, [], [], zip_fun)
1274+
do_zip_next(zips, acc, callback, [], [], zip_fun)
12241275
catch
12251276
kind, reason ->
12261277
do_zip_close(zips)
12271278
:erlang.raise(kind, reason, __STACKTRACE__)
12281279
else
12291280
{:next, buffer, acc} ->
1230-
do_zip(buffer, acc, callback, zip_fun)
1281+
do_zip_enum(buffer, acc, callback, zip_fun)
12311282

12321283
{:done, _acc} = other ->
12331284
other
12341285
end
12351286
end
12361287

1237-
# do_zip_next_tuple/6 computes the next tuple formed by
1288+
# do_zip_next/6 computes the next tuple formed by
12381289
# the next element of each zipped stream.
1239-
defp do_zip_next_tuple(
1290+
defp do_zip_next(
12401291
[{_, [], :halt} | zips],
12411292
acc,
12421293
_callback,
@@ -1248,15 +1299,15 @@ defmodule Stream do
12481299
{:done, acc}
12491300
end
12501301

1251-
defp do_zip_next_tuple([{fun, [], :cont} | zips], acc, callback, yielded_elems, buffer, zip_fun) do
1302+
defp do_zip_next([{fun, [], :cont} | zips], acc, callback, yielded_elems, buffer, zip_fun) do
12521303
case fun.({:cont, []}) do
12531304
{:suspended, [elem | next_acc], fun} ->
12541305
next_buffer = [{fun, next_acc, :cont} | buffer]
1255-
do_zip_next_tuple(zips, acc, callback, [elem | yielded_elems], next_buffer, zip_fun)
1306+
do_zip_next(zips, acc, callback, [elem | yielded_elems], next_buffer, zip_fun)
12561307

12571308
{_, [elem | next_acc]} ->
12581309
next_buffer = [{fun, next_acc, :halt} | buffer]
1259-
do_zip_next_tuple(zips, acc, callback, [elem | yielded_elems], next_buffer, zip_fun)
1310+
do_zip_next(zips, acc, callback, [elem | yielded_elems], next_buffer, zip_fun)
12601311

12611312
{_, []} ->
12621313
# The current zipped stream terminated, so we close all the streams
@@ -1266,7 +1317,7 @@ defmodule Stream do
12661317
end
12671318
end
12681319

1269-
defp do_zip_next_tuple(
1320+
defp do_zip_next(
12701321
[{fun, zip_acc, zip_op} | zips],
12711322
acc,
12721323
callback,
@@ -1276,10 +1327,10 @@ defmodule Stream do
12761327
) do
12771328
[elem | rest] = zip_acc
12781329
next_buffer = [{fun, rest, zip_op} | buffer]
1279-
do_zip_next_tuple(zips, acc, callback, [elem | yielded_elems], next_buffer, zip_fun)
1330+
do_zip_next(zips, acc, callback, [elem | yielded_elems], next_buffer, zip_fun)
12801331
end
12811332

1282-
defp do_zip_next_tuple([] = _zips, acc, callback, yielded_elems, buffer, zip_fun) do
1333+
defp do_zip_next([] = _zips, acc, callback, yielded_elems, buffer, zip_fun) do
12831334
# "yielded_elems" is a reversed list of results for the current iteration of
12841335
# zipping. That is to say, the nth element from each of the enums being zipped.
12851336
# It needs to be reversed and passed to the zipping function so it can do it's thing.

0 commit comments

Comments
 (0)