tomekowal
Background: I am writing a multiplayer mini-game with math puzzles. I need the puzzles to be random but reproducible given a seed. I decided to model them as an infinite Stream. The players will ask the stream for new puzzles. I did something like that but for one player in Elm long time ago.
http://tomekowal.github.io/elm-multiplication-game/
Now, the real question is: how do I get elements from the Stream one by one? I’d love an API similar to String.next_grapheme/1
String.next_grapheme("asdf")
{"a", "sdf"}
String.next_grapheme("")
nil
{1, stream} = Stream.next([1, 2, 3])
{2, stream} = Stream.next(stream)
{3, stream} = Stream.next(stream)
nil = Stream.next(stream)
Unfortunately, there is no such API neither in Stream nor in Enum modules. It seems that the question is fairly popular:
https://github.com/elixir-lang/elixir/issues/2515
And there are a couple of solutions to it:
https://github.com/tallakt/stream_split
https://github.com/hamiltop/streamz
https://github.com/Qqwy/elixir-iter
None of them is perfect. E.g. StreamSplit works on infinite Streams but breaks on lists.
Did you encounter such a problem? How do you code around it?
When I started, I was pretty sure something like that is in the standard library but I was wrong.
Is there a reason it is not there? Maybe it doesn’t play well with Stream.interval?
Trending in Questions
Other Trending Topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #library
- #deployment
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #elixirconf
- #channels
- #exunit
- #discussion
- #code-sync
- #podcasts
- #javascript
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixirconf-us
- #ai
- #blog-post
- #elixir-ls
- #phoenix_html
- #iex
- #graphql
- #genstage
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #hex
- #security
- #metaprogramming










Showing Posts 1 to 10- Show Best Posts
- Show All (oldest first)
- Show All (newest first)
sasajuric
I think you can find an example in gen_stage code.
tomekowal
Exactly, that is another example of the same problem. The protocol isn’t very complicated but wrong implementations can have subtle bugs like the one in
StreamSplit(or maybe it is not a bug, it is just not intended to use it with finite enumerables).The pattern is very common. Do you think it makes sense to make it part of the standard library? I am hesitant to open a proposal because every new feature needs to be maintained. But I feel, that without it, Stream API is not feature complete.
Imagine GenStage not knowing about the details of
Enumerableporotocol. Instead, it would useStream.next(enumerable, number_of_elements) -> {stream, values}wherestreamis nil if it ended.Apart from
Stream.next/2I’d go withStream.next/1which returns{stream, value}ornil.But before discussing API or implementation details, my question is. Do you think such “iterator” should be part of the standard library? Do you see any cons?
sasajuric
IMO, the ability to pull items from the stream one at a time definitely looks useful. I guess your questions should be addressed at @josevalim and the rest of the core team
Qqwy
Consuming a collection (both finite and infinite ones) one-by-one is definitely useful. I have had conversations with @josevalim about this before. The reason
Enum(and, by extension,Stream) do not support this by default is twofold:Enumerable.reduce), which always consumes the complete collection. For certain kinds of Enumerables, it is impossible or impractical to take elements out one by one. An example are file streams: iterating them one-by-one means that you have to keep the file open for the whole time you take elements out one-by-one.Nevertheless, there are cases like in your use-case where this is very useful. You mentioned the
iterrepository in your post. That is an old attempt at fleshing out what later became the Extractable library/protocol.Extractable only comes with out-of-the-box implementations for Lists, Maps and MapSets.
I am fairly certain that you could create a custom struct for your puzzle generating logic that contains a function which returns a
{puzzle, new_struct_with_function_for_next_puzzle}.tomekowal
Thank you! That is a handy insight!
ad1) File streams are a perfect point for not putting those functions in the standard API. It could encourage users to get lines one by one and do operations on them instead of composing the entire stream and then executing.
On the other hand, it might be useful for something like
tail -fwhere we know we don’t want to close the file.I thought a little about the
Stream.intervaland I figured that potentialStream.nextshould block until it can get all the elements, it tries to consume. It would make sense for bothintervalandtail -f.Stream.nextwould wait for one tick ofintervalor a new line appended to a file in thetail -f.I think with good examples and documentation, we should be able to show where to use and where not to use
Stream.nextad2) I wouldn’t base Enumerable on top of taking one element at a time. I wouldn’t touch Enumerable at all. It supports continuations and suspends, so I don’t think anything would change in the existing code.
WRT
Extractablelibrary: did you think about basing it on top of theEnumerable? It could use the same trick as the one used in thegen_stagecode that @sasajuric linked above. This way, it could automagically work for all kinds ofEnumerables.WRT puzzle generator. You are right. I could go with a custom generator, but I would miss all the potential that the
Streammodule gives me. Maybe I could enable an easy mode where someone needs to only solve every second puzzle withStream.take_every. Alternatively, instead of generating puzzles one from another, I could create a stream of seeds and build puzzles from that:I’d instead implement the
nextfunction in my project withEnumerablelike in theGenStageexample to preserve all the powerStreamgives me.I’d like to know of other downsides you can think of. I am more and more inclined to create a proposal, and this kind of feedback is helpful.
Qqwy
Thank you; very interesting thoughts
.
Correct, but in a situation like a
tail -f, what we are actually modeling is much closer to e.g. GenStage, where we have multiple producers/consumers in parallel.This is very different from streams like your PuzzleStream, which are (potentially) infinite, enumerated one-at-a-time and not dependent on the production of a different process.
The
GenStage.Streamercode that @sasajuric linked to turns an (potentially infinite) local stream in a process-wrapped GenStage producer. This is an interesting ‘solution’, but it does mean that we have the extra overhead of a process when we want to work with such a stream. I think that there are many instances where a pure approach that keeps everything inside the same process makes more sense.Related; I am reluctant to offer a seemingly data-only API which behind the scenes spins up processes.
But that does mean that building an abstraction that would happily work for arbitrary existing Enumerables (and which does not spin up processes) is difficult and probably impossible to implement.
WRT the PuzzleGenerator: I think that we could easily (and it would be an interesting exercise!) create a bunch of functions that allow a similar interface to what Stream provides. Maybe it is even possible to create a variant that is fully pipe-able. (Extracting single elements, of course, is something probably more commonly done in a
with-statement).tomekowal
OK, I started testing it and it is much harder than I expected
a) I can’t treat suspended stream a.k.a. continuation as another stream. If I start iterating I need to finish and can’t apply any other transformations so
In example list
{value, stream} = Stream.next(stream)doesn’t work. It can be{value, continuation} = MyStream.next(stream_or_continuation)b) the continuations were not designed to call one by one
[1,2,3]results in four steps:[1,2,3]returning{:suspeneded, 1, continuation}[2, 3]returning{:suspended, 2, continuation}[3]returning{:suspended, 3, continuation}[]returning{:done, whatever we think should indicate no value}but
[1,2,3] |> Stream.take(3)results in three steps:[1,2,3]returning{:suspended, 1, continuation}[2, 3]returning{:suspended, 2, continuation}[3]returning{:halted, 3}So I need to make sure the code handles it. Here is what I came up with:
tomekowal
Oh, and sometimes halting retunrs an element while sometimes it doesn’t and only copies value from given accumulator (e.g. File.stream! does that), so I came up with something crazy like this:
I am going to sleep on it
liskin
There is one additional reason why streams can’t be consumed one-by-one and instead need to be folded (
Enumerable.reduced) at once: the stream generator may have side-effects (example: consumingFile.stream!advances the file position) and therefore must be invoked at most once.Try this:
Assuming
test.txtcontains12345, one would expect to see122but instead you get123.The
StreamSplitlibrary suffers from the same problem:The documentation for
Enumerablerepeatedly mentions that “In case areducer/0function returns the:suspendaccumulator, the:suspendedtuple must be explicitly handled by the caller and never leak.” And that’s exactly what your code does — the continuation leaks and can then be called multiple times, messing up the side effects.If one comes from a Haskell background (like me), it really is somewhat suprising.
Streamresembles a lazily evaluated linked list, but the BEAM VM doesn’t have lazy evaluation and thunks, so the side-effectfulStreammust be treated with care.Here’s my stab at a safe-ish stream iterator:
It’s just a hack, the continuation kind of leaks as well, but this leak is protected by
callable_oncewhich raises whenever the possibly side-effecting fun is invoked more than once.tomekowal
Wow! This
callable_onceis brilliant! I haven’t heard about:atomicsbefore.And now I understand better why passing a continuation around might result in shooting myself in the foot
I find this funny: my initial idea for the stream of puzzles was precisely about two processes iterating over the same stream at different speeds
In my case, I could create two identical streams, and that should work.
For infinite streams without side effects like the stream of puzzles or
Stream.cycle([1,2,3])setting atomics references seems wasteful.BUT, if we would like to introduce
Strem.nextorStream.stepto standard API, we would have to let theEnumerabledecide, if it uses side effects and needs protection or if it is safe to call the same continuation multiple times.That would require adding protections as
callable_onceto all side effectful streams in the stdlib and changes in the API of generators likeStrem.resourceorStrem.iterateto enable such protections. Well, evenStream.mapcould have side effects that we might want to control.Suddenly the amount of work to implement it becomes more significant. Next question arises: is it worth it?
The amount of libraries and even GenStage example suggests there is a need for something like that while it is very easy to do wrong.
On the other hand, that would complicate the
StreamAPI and add another thing to think about when implementingEnumberableprotocol.From those two, I think complicating the
StreamAPI is a bigger issue. It is elegant and clean, so addingoptslikeprotect: trueseems like a bad idea to me. And of course, allowing continuations to leak is even worse.Alternatively, we could keep protections for all streams as in your solution, and never allow calling the same continuation twice even when it is safe. That would require information that it might be less efficient than regular processing. Those
:atomicsinstructions should be fast though.Well, at least I now know why the stepping API is hard to implement
I still don’t want to give up on the idea of my
PuzzleStreambeing a properStream, but maybe instead of stepping over it, I’ll implement the game logic insideStream.transform/3Thank you all for the discussion! It was enlightening!