Yes, some of them may consume it at all or cycle, but these are somewhat clear (in my opinion). Negative indexes by definition imply going to the end of the collection. Cycle by definition implies multiple traversals. But I don't think unzip necessarily implies multiple traversals and we are not gaining anything compared to calling Stream.map (compared to Enum.unzip, which actually does it in a single pass).
*José Valimhttps://dashbit.co/ <https://dashbit.co/>* On Tue, Sep 23, 2025 at 3:08 PM Nilanjan De <[email protected]> wrote: > 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 > <https://groups.google.com/d/msgid/elixir-lang-core/CAOgvJOKS8Oq2TwVjSGEqEXwiMzLw1oFt_Sy45aYmdCwjyWMsNA%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/CAGnRm4KDTHesoQm5TGMd9Nnx-1oz81%2BPeOhmU%2BSFw3KaHu5ebA%40mail.gmail.com.
