Indeed my implementation does traverse the enumerable twice. Trying to implement this in a single traversal is problematic since one side may be consumed without the other, therefore, a single-pass must either buffer unboundedly for the slower side or block the faster side, neither of which is not desirable. I agree the “two streams” shape can surprise, that’s why I mentioned the double traversal explicitly in the docstring.
Re: “breaking Stream properties”: From my reading of the docs and code, I think Stream guarantees laziness and composability, not a universal single-pass across multiple, independent consumers. For example, Stream.cycle/1 re-enumerates its source by design; some ops (take) with negative counts also fully consume the input (with bounded memory) before producing, (which is problematic on infinite streams hence called out in the docs). These are examples which show that Stream doesn’t promise “emit as you read once.”. On Tue, Sep 23, 2025 at 5:26 PM José Valim <[email protected]> wrote: > I am not sure if it ever existed. The issue with the implementation above > and any other streaming unzip implementation is that it will ultimately > traverse the enumerable twice. If that's an okay compromise for your use > case, then you can call `Stream.map` yourself. But generally speaking, it > does break the properties of the Stream module. > > > *José Valimhttps://dashbit.co/ <https://dashbit.co/>* > > > On Tue, Sep 23, 2025 at 1:49 PM Nilanjan De <[email protected]> > wrote: > >> I am learning Elixir and was going through this tutorial >> <https://elixir-phoenix-ash.com/elixir/index.html#stream-unzip> which >> mentions the Stream.unzip/1 function but I noticed that Stream.unzip/1 does >> not exist in the std library. >> >> Was it present earlier and was deprecated or was it never added or was it >> decided not to add it for some reason? >> >> If it makes sense to add this to the std library, happy to send a PR for >> review. >> - >> https://github.com/n1lanjan/elixir/commit/f720477f21feebc938ed9effd58bf16fd04e0089 >> >> >> ```diff >> diff --git a/lib/elixir/lib/stream.ex b/lib/elixir/lib/stream.ex >> index 81704f1e3..79622cd95 100644 >> --- a/lib/elixir/lib/stream.ex >> +++ b/lib/elixir/lib/stream.ex >> @@ -1383,6 +1383,38 @@ def zip_with(enumerables, zip_fun) do >> R.zip_with(enumerables, zip_fun) >> end >> >> + @doc """ >> + Opposite of `zip/2`. Lazily splits a stream of two-element tuples into >> two streams. >> + >> + It returns a tuple with two streams. Each stream enumerates the >> corresponding >> + element of the input tuples. >> + >> + Each returned stream enumerates the input independently. Enumerating >> both >> + streams will traverse the input twice. If your input is a resource or >> costs >> + to enumerate, consider materializing once with `Enum.unzip/1`. >> + >> + This function expects elements to be two-element tuples. Otherwise, it >> will >> + fail at enumeration time. >> + >> + ## Examples >> + >> + iex> {left, right} = Stream.unzip([{:a, 1}, {:b, 2}, {:c, 3}]) >> + iex> Enum.to_list(left) >> + [:a, :b, :c] >> + iex> Enum.to_list(right) >> + [1, 2, 3] >> + >> + """ >> + @doc since: "1.19.0" >> + @spec unzip(Enumerable.t({left, right})) :: {Enumerable.t(left), >> Enumerable.t(right)} >> + when left: term, right: term >> + def unzip(enumerable) do >> + { >> + map(enumerable, fn {left, _right} -> left end), >> + map(enumerable, fn {_left, right} -> right end) >> + } >> + end >> + >> ## Sources >> >> @doc """ >> diff --git a/lib/elixir/test/elixir/stream_test.exs >> b/lib/elixir/test/elixir/stream_test.exs >> index ed62a1cfa..297e4b729 100644 >> --- a/lib/elixir/test/elixir/stream_test.exs >> +++ b/lib/elixir/test/elixir/stream_test.exs >> @@ -1342,6 +1342,53 @@ test "zip_with/2 does not leave streams suspended >> on halt" do >> assert Process.get(:stream_zip_with) == :done >> end >> >> + test "unzip/1 is lazy" do >> + {left, right} = Stream.unzip([{:a, 1}]) >> + assert lazy?(left) >> + assert lazy?(right) >> + end >> + >> + test "unzip/1 basic" do >> + {left, right} = Stream.unzip([{:a, 1}, {:b, 2}, {:c, 3}]) >> + assert Enum.to_list(left) == [:a, :b, :c] >> + assert Enum.to_list(right) == [1, 2, 3] >> + end >> + >> + test "unzip/1 enumerates the input independently for each side" do >> + Process.put(:stream_unzip_calls, 0) >> + >> + source = >> + Stream.map([{:a, 1}, {:b, 2}], fn tuple -> >> + Process.put(:stream_unzip_calls, >> Process.get(:stream_unzip_calls) + 1) >> + tuple >> + end) >> + >> + {left, right} = Stream.unzip(source) >> + assert Enum.to_list(left) == [:a, :b] >> + assert Enum.to_list(right) == [1, 2] >> + assert Process.get(:stream_unzip_calls) == 4 >> + end >> + >> + test "unzip/1 roundtrips with zip/2" do >> + concat = Stream.concat(1..3, 4..6) >> + cycle = Stream.cycle([:a, :b, :c]) >> + zipped = Stream.zip(concat, cycle) >> + >> + {left, right} = Stream.unzip(zipped) >> + assert Enum.to_list(Stream.zip(left, right)) == Enum.to_list(zipped) >> + end >> + >> + test "unzip/1 raises on non-tuple elements at enumeration time" do >> + {left, _right} = Stream.unzip([:a, :b, :c]) >> + assert_raise FunctionClauseError, fn -> Enum.to_list(left) end >> + end >> + >> + test "unzip/1 on empty input" do >> + {left, right} = Stream.unzip([]) >> + assert Enum.to_list(left) == [] >> + assert Enum.to_list(right) == [] >> + end >> + >> test "zip_with/2 closes on inner error" do >> zip_with_fun = &List.to_tuple/1 >> stream = Stream.into([1, 2, 3], %Pdict{}) >> ``` >> >> -- >> You received this message because you are subscribed to the Google Groups >> "elixir-lang-core" group. >> To unsubscribe from this group and stop receiving emails from it, send an >> email to [email protected]. >> To view this discussion visit >> https://groups.google.com/d/msgid/elixir-lang-core/2c29ac4a-736a-4cc5-b4db-f2022dd8411bn%40googlegroups.com >> <https://groups.google.com/d/msgid/elixir-lang-core/2c29ac4a-736a-4cc5-b4db-f2022dd8411bn%40googlegroups.com?utm_medium=email&utm_source=footer> >> . >> > -- > You received this message because you are subscribed to the Google Groups > "elixir-lang-core" group. > To unsubscribe from this group and stop receiving emails from it, send an > email to [email protected]. > To view this discussion visit > https://groups.google.com/d/msgid/elixir-lang-core/CAGnRm4JkyaiUsYj6RpZ41T7OzJfukYexU3QcDrTpjcG2ReZRDA%40mail.gmail.com > <https://groups.google.com/d/msgid/elixir-lang-core/CAGnRm4JkyaiUsYj6RpZ41T7OzJfukYexU3QcDrTpjcG2ReZRDA%40mail.gmail.com?utm_medium=email&utm_source=footer> > . > -- You received this message because you are subscribed to the Google Groups "elixir-lang-core" group. To unsubscribe from this group and stop receiving emails from it, send an email to [email protected]. To view this discussion visit https://groups.google.com/d/msgid/elixir-lang-core/CAOgvJOKS8Oq2TwVjSGEqEXwiMzLw1oFt_Sy45aYmdCwjyWMsNA%40mail.gmail.com.
