Stream.transform/4 is now aware of the acc’s tail, if presented#5680
Stream.transform/4 is now aware of the acc’s tail, if presented#5680am-kantox wants to merge 2 commits intoelixir-lang:masterfrom am-kantox:make-stream-transform-care-of-tail
Stream.transform/4 is now aware of the acc’s tail, if presented#5680Conversation
|
The problem with |
|
This looks like a chicken-egg problem and I do not see any harm in having a particularly dedicated value to be a show-stopper element. {_, []} when is_list(user_acc) ->
do_list_transform(user_acc, user, fun, :halt, next, inner_acc, inner,
&Enumerable.List.reduce([user_acc], &1, fun), after_fun)Instead of the It’s evidently up to you to decide, but I like the approach with |
|
Throughout the whole design of the Stream library we have been very careful to not introduce situations where there is ambiguity: i.e. situations where it would be unclear if a given atom is part of a collection or an special token emitted by the stream system. The proper way to solve this would be by using tuples or introducing a new callback. We won't accept any solution where the meaning of atoms are overloaded, as that will eventually turn out to be a bug or source of confusion. |
|
OK, now it’s just silently taking care about the tail being also included into the result. |
|
@am-kantox unfortunately we cannot assume such as well. We cannot assume that every time the user accumulator is a list that it is something that is meant to be appended as is to the result. We need a general solution, otherwise someone that has the same needs but keeps the accumulator as a tuple or a map, because they need to keep track of more information, is still going to run into issues. |
|
@josevalim ok, I see what you are saying. I will do my best to implement a |
|
@am-kantox you don't need to change streams/reducers.ex as those are only for reducers shared between Enum and Stream. In any case, the only stream that supports "resolving" an accumulator is the chunk one, so I would look at it for ideas. |
Stream.transform/3 is now aware of the acc’s tail, if presented.Stream.transform/3 is now aware of the acc’s tail, if presented
|
@josevalim I ran into this today as well. What about making it so that the ie. instead of discarding |
|
@josevalim ok, now I see what you were saying. I am in the middle of refactoring, I don’t give up. At the moment I have an amount of questions: — is there any reason for having defp lazy?(stream) do
- match?(%Stream{}, stream) or is_function(stream, 2)
+ match?(%Stream{}, stream)
endI understand that requires more refactoring, but in my opinion it worth it. — how do we suppose to distinguish tail to be emitted from “just the last accumulator value”?
— what do you think about adding two new members to At the moment that’s it. I should be able to introduce the approach this weekend. |
|
@rschmukler |
Just to make it clear: the concatenated with the rest and possibly reversed should be done by your code and not by the Stream code. If Stream does it, then it has to assume something about the accumulator but it should never do that. So for example, allowing after_fun to return something like
The function/2 is a convenience so you are not forced to create |
|
OK, then maybe the easiest solution would be: if and only defp do_after(nil, {acc, _user_acc}), do: acc
defp do_after(fun, {acc, user_acc}) do
case fun.(user_acc) do
finalize when is_function(finalize, 1) -> finalize.(acc)
_ -> acc
end
end# for the case of acc is `List`, this might be passed:
after_fun = fn rest -> fn acc -> [rest | acc] end endI can’t see any drawbacks of this solution. Tests are passed. |
Stream.transform/3 is now aware of the acc’s tail, if presentedStream.transform/4 is now aware of the acc’s tail, if presented
|
@am-kantox I'm curious, what are the advantages of returning a |
|
@rschmukler in my opinion, it is more flexible in terms of consumer’s code. The only thing at this very moment, that matters, is the last value of |
|
@am-kantox The @josevalim any thoughts on the proposed APIs? |
|
Fwiw, the problem with the after_fun is that it is called even in case of errors, where you don't care about the accumulator or where it may be even wrong. So an extra function may be the way to go. |
|
Fair point, then @am-kantox solution looks good to me 👍 |
|
@am-kantox and @rschmukler would your solution be solved by a custom chunk_by function that also receives the accumulator? 1..10
|> Stream.chunk_by([], fn i, acc ->
if rem(i, 2) == 0, do: {Enum.reverse([i | acc]), []}, else: {[], [i | acc]}
end, fn acc -> Enum.reverse(acc) end)
|> Enum.to_list
#=> [[1, 2], [3, 4], [5, 6], [7, 8], ...] |
|
My particular problem would be solved, but I still think that (1..10) |> Stream.transform([], fn i, acc -> {[], [i | acc]} end) |> Enum.to_list
#⇒ []I think that to make the statement “ |
|
@am-kantox changing Stream.transform/4 to handle this concern seems to be too complicated and prone for failures. I am running some tests on this PR and there are many situations where the accumulator is being lost, exactly because there are two loops to consider here. The code in transform/4 is already very complex and I am not looking forward to add more to it. Especially as |
|
@josevalim I am 100% in agreement with “not adding more complexity to |
|
Let's go with Stream.chunk_by/4 then. Closing this in favor of #5888. Thank you! |
Stream.transform/3in current implementation was unaware of the tail, left in the accumulator:Within this patch, if the accumulator is there,
do_transform_useris called once more time with:donestatus, making it possible to:I am not sure about
:done, but I think it makes sense in general.