Skip to content

Commit 4a2d1d6

Browse files
author
José Valim
committed
Add Stream.chunks_by/2
1 parent 1b93904 commit 4a2d1d6

File tree

4 files changed

+101
-44
lines changed

4 files changed

+101
-44
lines changed

lib/elixir/lib/enum.ex

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -340,24 +340,14 @@ defmodule Enum do
340340
"""
341341
@spec chunks_by(t, (element -> any)) :: [list]
342342
def chunks_by(coll, fun) do
343-
res =
344-
reduce(coll, nil, fn
345-
x, {acc, buffer, value} ->
346-
new_value = fun.(x)
347-
if new_value == value do
348-
{ acc, [x | buffer], value }
349-
else
350-
{ [:lists.reverse(buffer) | acc], [x], new_value }
351-
end
352-
x, nil ->
353-
{ [], [x], fun.(x) }
354-
end)
343+
{ _, { acc, res } } =
344+
Enumerable.reduce(coll, { :cont, { [], nil } }, R.chunks_by(fun))
355345

356-
if nil?(res) do
357-
[]
358-
else
359-
{ acc, buffer, _ } = res
360-
:lists.reverse([:lists.reverse(buffer) | acc])
346+
case res do
347+
{ buffer, _ } ->
348+
:lists.reverse([:lists.reverse(buffer) | acc])
349+
nil ->
350+
[]
361351
end
362352
end
363353

lib/elixir/lib/stream.ex

Lines changed: 62 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ defmodule Stream do
8383
like `Stream.cycle/1`, `Stream.unfold/2`, `Stream.resource/3` and more.
8484
"""
8585

86-
defrecord Lazy, enum: nil, funs: [], accs: []
86+
defrecord Lazy, enum: nil, funs: [], accs: [], done: nil
8787

8888
defimpl Enumerable, for: Lazy do
8989
@compile :inline_list_funs
@@ -103,25 +103,36 @@ defmodule Stream do
103103
{ :error, __MODULE__ }
104104
end
105105

106-
defp do_reduce(Lazy[enum: enum, funs: funs, accs: accs], acc, fun) do
106+
defp do_reduce(Lazy[enum: enum, funs: funs, accs: accs, done: done], acc, fun) do
107107
composed = :lists.foldl(fn fun, acc -> fun.(acc) end, fun, funs)
108-
do_each(&Enumerable.reduce(enum, &1, composed), :lists.reverse(accs), acc)
108+
do_each(&Enumerable.reduce(enum, &1, composed), done && { done, fun }, :lists.reverse(accs), acc)
109109
end
110110

111-
defp do_each(_reduce, _accs, { :halt, acc }) do
111+
defp do_each(_reduce, _done, _accs, { :halt, acc }) do
112112
{ :halted, acc }
113113
end
114114

115-
defp do_each(reduce, accs, { :suspend, acc }) do
116-
{ :suspended, acc, &do_each(reduce, accs, &1) }
115+
defp do_each(reduce, done, accs, { :suspend, acc }) do
116+
{ :suspended, acc, &do_each(reduce, done, accs, &1) }
117117
end
118118

119-
defp do_each(reduce, accs, { :cont, acc }) do
119+
defp do_each(reduce, done, accs, { :cont, acc }) do
120120
case reduce.({ :cont, [acc|accs] }) do
121-
{ reason, [acc|_] } ->
122-
{ reason, acc }
123121
{ :suspended, [acc|accs], continuation } ->
124-
{ :suspended, acc, &do_each(continuation, accs, &1) }
122+
{ :suspended, acc, &do_each(continuation, done, accs, &1) }
123+
{ :halted, [acc|_] } ->
124+
{ :halted, acc }
125+
{ :done, [acc|_] = accs } ->
126+
case done do
127+
nil ->
128+
{ :done, acc }
129+
{ done, fun } ->
130+
case done.(fun).(accs) do
131+
{ :cont, [acc|_] } -> { :done, acc }
132+
{ :halt, [acc|_] } -> { :halted, acc }
133+
{ :suspend, [acc|_] } -> { :suspended, acc, &({ :done, &1 |> elem(1) }) }
134+
end
135+
end
125136
end
126137
end
127138
end
@@ -151,6 +162,33 @@ defmodule Stream do
151162

152163
## Transformers
153164

165+
@doc """
166+
Chunk the `enum` by buffering elements for which `fun` returns
167+
the same value and only emit them when `fun` returns a new value
168+
or the `enum` finishes,
169+
170+
## Examples
171+
172+
iex> stream = Stream.chunks_by([1, 2, 2, 3, 4, 4, 6, 7, 7], &(rem(&1, 2) == 1))
173+
iex> Enum.to_list(stream)
174+
[[1], [2, 2], [3], [4, 4, 6], [7, 7]]
175+
176+
"""
177+
@spec chunks_by(Enumerable.t, (element -> any)) :: Enumerable.t
178+
def chunks_by(enum, fun) do
179+
lazy enum, nil,
180+
fn(f1) -> R.chunks_by(fun, f1) end,
181+
fn(f1) -> &do_chunks_by(&1, f1) end
182+
end
183+
184+
defp do_chunks_by(acc(_, nil, _) = acc, _f1) do
185+
{ :cont, acc }
186+
end
187+
188+
defp do_chunks_by(acc(h, { buffer, _ }, t), f1) do
189+
cont_with_acc(f1, :lists.reverse(buffer), h, nil, t)
190+
end
191+
154192
@doc """
155193
Lazily drops the next `n` items from the enumerable.
156194
@@ -720,23 +758,20 @@ defmodule Stream do
720758

721759
## Helpers
722760

723-
@compile { :inline, lazy: 2, lazy: 3 }
761+
@compile { :inline, lazy: 2, lazy: 3, lazy: 4 }
724762

725-
defp lazy(enum, fun) do
726-
case enum do
727-
Lazy[funs: funs] = lazy ->
728-
lazy.funs([fun|funs])
729-
_ ->
730-
Lazy[enum: enum, funs: [fun], accs: []]
731-
end
732-
end
763+
defp lazy(Lazy[funs: funs] = lazy, fun),
764+
do: lazy.funs([fun|funs])
765+
defp lazy(enum, fun),
766+
do: Lazy[enum: enum, funs: [fun]]
733767

734-
defp lazy(enum, acc, fun) do
735-
case enum do
736-
Lazy[funs: funs, accs: accs] = lazy ->
737-
lazy.funs([fun|funs]).accs([acc|accs])
738-
_ ->
739-
Lazy[enum: enum, funs: [fun], accs: [acc]]
740-
end
741-
end
768+
defp lazy(Lazy[funs: funs, accs: accs] = lazy, acc, fun),
769+
do: lazy.funs([fun|funs]).accs([acc|accs])
770+
defp lazy(enum, acc, fun),
771+
do: Lazy[enum: enum, funs: [fun], accs: [acc]]
772+
773+
defp lazy(Lazy[done: nil, funs: funs, accs: accs] = lazy, acc, fun, done),
774+
do: lazy.funs([fun|funs]).accs([acc|accs]).done(done)
775+
defp lazy(enum, acc, fun, done),
776+
do: Lazy[enum: enum, funs: [fun], accs: [acc], done: done]
742777
end

lib/elixir/lib/stream/reducers.ex

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,22 @@ defmodule Stream.Reducers do
22
# Collection of reducers shared by Enum and Stream.
33
@moduledoc false
44

5+
defmacro chunks_by(fun, f // nil) do
6+
quote do
7+
fn
8+
entry, acc(h, { buffer, value }, t) ->
9+
new_value = unquote(fun).(entry)
10+
if new_value == value do
11+
{ :cont, acc(h, { [entry|buffer], value }, t) }
12+
else
13+
cont_with_acc(unquote(f), :lists.reverse(buffer), h, { [entry], new_value }, t)
14+
end
15+
entry, acc(h, nil, t) ->
16+
{ :cont, acc(h, { [entry], unquote(fun).(entry) }, t) }
17+
end
18+
end
19+
end
20+
521
defmacro drop(f // nil) do
622
quote do
723
fn

lib/elixir/test/elixir/stream_test.exs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,22 @@ defmodule StreamTest do
2525
assert Enum.to_list(stream) == [3,5,7]
2626
end
2727

28+
test "chunks_by" do
29+
stream = Stream.chunks_by([1, 2, 2, 3, 4, 4, 6, 7, 7], &(rem(&1, 2) == 1))
30+
31+
assert is_lazy(stream)
32+
assert Enum.to_list(stream) ==
33+
[[1], [2, 2], [3], [4, 4, 6], [7, 7]]
34+
assert stream |> Stream.take(3) |> Enum.to_list ==
35+
[[1], [2, 2], [3]]
36+
end
37+
38+
test "chunks_by is zippable" do
39+
stream = Stream.chunks_by([1, 2, 2, 3], &(rem(&1, 2) == 1))
40+
list = Enum.to_list(stream)
41+
assert Enum.zip(list, list) == Enum.zip(stream, stream)
42+
end
43+
2844
test "concat/1" do
2945
stream = Stream.concat([1..3, [], [4, 5, 6], [], 7..9])
3046
assert is_function(stream)

0 commit comments

Comments
 (0)