By using Elixir Streams, you can make your application more responsive and consume fewer resources. Within this session, I shall explain how Streams work, and provide a few practical uses of Streams, with a live demonstration.
Finite Enumerables Lists, Maps, Keywords, etc. are finite Enumerables, with contents generated ahead of time All of them can be enumerated in the same manner — in linear or constant time, depending on the structure — because they all implement the Enumerable protocol
Lazy Enumerables Any data type which has an implementation of the Enumerable protocol can be enumerated. This attribute can in turn be used to create lazily evaluated Enumerables.
Lazy Enumerables Lazy Enumerables can act as placeholders for actual content which minimises blocking Computation is spread throughout the lifetime of the enumerable and can be halted if the results are no longer required
Anything is Enumerable > Since enumerables can have different shapes (structs, anonymous functions, and so on), the functions in this module may return any of those shapes and this may change at any time.
Enum.reduce/3 The core concept which drives all enumeration Can be used to implement all other Enum functions Can be replaced with optimisation paths Calls Enumerable.reduce/3
Enumerable Protocol Defines a series of functions to be implemented by any data type that wishes to become enumerable Includes reduce/3, count/1, member?/2, slice/1 with reduce/3 providing core functionality
Infinite Enumerables Any data type that implements Enumerable, and always returns new items in its reduce/3 implementation, is infinite. The Stream module provides convenience functions to construct such Enumerables!
Incremental Peeling Abuse of Enumerable.reduce/3 allows incremental consumption of any Enumerable, including Streams! Trick: Start the enumeration in a suspended state, accumulate into nil, but keep returning items Note: Streams might be non-reentrant
Note Streams are pull-based. No computation is done unless if new values are pulled from it. Collections are push-based. They can be used to collect output (e.g. File.Stream!/1)
Stream.resource/3 Creates initial accumulator in start_fun Calls next_fun repeatedly until completion Calls after_fun at the end of enumeration Can be used for single-pass (streaming) file generation
Task.async_stream/3 Emits a Stream which runs the given function once for each element in the Enumerable With options: max_concurrency, ordered Executed with Stream.run/1
Plug.Conn.chunk/2 Sends the response to the client incrementally Requires a connection which has been configured with send_chunked/2 Leaves enumeration to the programmer
StreamData use ExUnitProperties property "bin1 <> bin2 always starts with bin1" do check all bin1 <- binary(), bin2 <- binary() do assert String.starts_with?(bin1 <> bin2, bin1) end end
Why Use Streams? Minimise latency: Reduce time-to-first-byte and minimise jank risk (system saturation) Reduce resource usage: Eliminate peaks and avoid wasted work Reduce complexity: Leverage composition for shorter and more succinct programs