Current section
Files
Jump to
Current section
Files
src/stream.gleam
//// `glimmer/stream` introduces an abstraction over `gleam/erlang/process`, called a Stream.
//// The goal of streams are to make parallelism free and easy by having it appear sequential.
//// There are both functional and imperative (effectful) functions in this library.
//// The imperative functions are most useful for getting a stream started:
//// the options for filling up a fresh stream are basically just `write` which is imperative and
//// `from_list` which is functional, and `write` will make more sense for many situations.
//// Once a stream is going, and you just need to process it, then the functional functions
//// (`map`, `filter`, `reduce`, `collect`, `to_iterator`) can be very ergonomic and sensible.
//// Note: streams masquerade as lists, but they aren't datastructures. Reading from them consumes the items,
//// which the functional functions get around by saving results in a new stream or datastructure.
//// If you want something like array-indexing then you'll need to convert the stream into an
//// actual datastructure, perhaps with `collect`, and likely lose any concurrency benefits.
import gleam/erlang/process
import gleam/iterator
import gleam/otp/task
import gleam/list
/// the types of messages used within Glimmer, for Streams.
pub opaque type PipeMessage(a) {
Another(val: a)
Done
}
/// A concurrent stream of values.
/// This isn't a datastructure per se, it's all lazy.
pub type Stream(a) {
Stream(process.Subject(PipeMessage(a)))
}
/// Construct a stream.
pub fn new() -> Stream(a) {
Stream(process.new_subject())
}
/// Construct a stream from a list.
pub fn from_list(l: List(a)) -> Stream(a) {
let s = new()
task.async(fn() { list.map(l, fn(a) { write(s, a) }) })
s
}
/// Get the next value from the stream.
/// Wait if there isn't one yet.
pub fn next(s: Stream(a)) -> Result(a, Nil) {
next_with_timeout(s, 15 * 60 * 1000)
}
/// Get the next value from the stream.
/// Wait if it isn't there yet, giving up if the timeout runs out.
pub fn next_with_timeout(s: Stream(a), timeout: Int) -> Result(a, Nil) {
let Stream(subject) = s
case process.receive(subject, timeout) {
Ok(Another(a)) -> {
Ok(a)
}
Ok(Done) -> {
Error(Nil)
}
Error(Nil) -> {
Error(Nil)
}
}
}
/// Write a value to a stream.
/// This is an imperative, side-effectful procedure.
pub fn write(s: Stream(a), value: a) -> Nil {
let Stream(subject) = s
process.send(subject, Another(value))
}
/// Indicate that no more values will be sent in the stream.
/// This is optional but useful if the stream is intended to model some finite datastructure,
/// which many of these functions expect.
/// This is an imperative, side-effectful procedure.
pub fn close(s: Stream(a)) -> Nil {
let Stream(subject) = s
process.send(subject, Done)
}
/// Represent a stream as a Gleam iterator.
pub fn to_iterator(s: Stream(a)) -> iterator.Iterator(a) {
iterator.unfold(
Nil,
fn(_) {
case next(s) {
Ok(a) -> {
iterator.Next(a, Nil)
}
Error(Nil) -> {
iterator.Done
}
}
},
)
}
/// Represent a stream as a list.
/// This blocks until there's an indication that the stream is over.
/// (See `close`)
pub fn collect(s: Stream(a)) -> List(a) {
to_iterator(s)
|> iterator.to_list()
}
/// Use a stream and then `close` it.
/// This is intended for `use` syntax, for example:
/// ```gleam
/// use <- with(output_stream)
/// write(out, "hi")
/// ```
pub fn with(stream: Stream(a), f: fn() -> b) -> b {
let out = f()
close(stream)
out
}
/// Perform a side-effect for each element in the stream, consuming it.
/// This effect can include writing to another stream,
/// so the elements aren't necessarily gone. For example:
/// ```gleam
/// use <- with(output_stream)
/// use i <- foreach(input_stream)
/// output_stream |> write(i * 2)
/// ```
pub fn foreach(stream: Stream(a), f: fn(a) -> Nil) -> Nil {
case next(stream) {
Ok(a) -> {
f(a)
foreach(stream, f)
}
Error(Nil) -> Nil
}
}
/// Iterate through the elements in the stream until done or `Error`.
/// Perform some computation each time.
/// For example:
/// ```gleam
/// use i <- foreach_unless_error(input_stream)
/// case i < 0 {
/// True -> Error("found a negative!")
/// False -> Ok(Nil)
/// }
/// ```
pub fn foreach_unless_error(
stream: Stream(a),
f: fn(a) -> Result(Nil, c),
) -> Result(Nil, c) {
case next(stream) {
Ok(a) -> {
case f(a) {
Ok(Nil) -> foreach_unless_error(stream, f)
Error(err) -> Error(err)
}
}
Error(Nil) -> Ok(Nil)
}
}
/// Map a function over a stream concurrently
/// (as opposed to, say, calling `collect` and then using `list.map`).
/// Internally there is imperative dark magic but this presents
/// a pure functional interface (if `f` is pure).
/// This makes it great for pipes. For example,
/// ```gleam
/// [1, 2, 3]
/// |> from_list()
/// |> map(fn(n) { n + 1 })
/// |> map(fn(n) { n * 2 })
/// |> collect()
/// |> io.debug() // prints [4, 6, 8]
/// ```
pub fn map(input: Stream(a), f: fn(a) -> b) -> Stream(b) {
let output = new()
task.async(fn() {
use <- with(output)
use a <- foreach(input)
write(output, f(a))
Nil
})
output
}
/// Filter elements out of a stream concurrently
/// (as opposed to, say, calling `collect` and then using `list.filter`).
/// Internally there is imperative dark magic but this presents
/// a pure functional interface (if `p` is pure).
/// This makes it great for pipes. For example,
/// ```gleam
/// [1, 2, 3]
/// |> from_list
/// |> filter(fn(n) { n % 2 == 0 })
/// |> collect()
/// |> io.debug() // prints [1, 3]
/// ```
pub fn filter(input: Stream(a), p: fn(a) -> Bool) -> Stream(a) {
let output = new()
task.async(fn() {
use <- with(output)
use a <- foreach(input)
case p(a) {
True -> write(output, a)
False -> Nil
}
})
output
}
/// Reduce (or fold) a stream to a value.
/// This is concurrent in the sense that reduction steps begin before
/// the last value arrives, and may happen in parallel.
/// However, the function won't return until all values are received, of course.
/// Internally there is imperative dark magic but this presents
/// a pure functional interface (if `f` is pure).
/// This makes it great for pipes. For example,
/// ```gleam
/// [1, 2, 3]
/// |> from_list
/// |> reduce(0, fn(a, b) { a + b })
/// |> io.debug() // prints 6
/// ```
pub fn reduce(input: Stream(a), start: b, f: fn(a, b) -> b) -> b {
case next(input) {
Ok(a) -> reduce(input, f(a, start), f)
Error(Nil) -> start
}
}
/// Generate a stream. For example:
/// ```gleam
/// fn
// pub fn generator(g: fn(fn(b)->Nil, fn()->Nil)->Nil) -> Stream(b) {
// let output = new()
// g(fn(b){ write(output, b) }, fn() {close(output)})
// output
// }