Skip to content

Instantly share code, notes, and snippets.

@robey
Last active April 10, 2017 20:00
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save robey/49de803bd4a8d7576de489575e609a95 to your computer and use it in GitHub Desktop.
Save robey/49de803bd4a8d7576de489575e609a95 to your computer and use it in GitHub Desktop.
Changing the shape of rust Future & Stream

Changing the shape of rust Future & Stream

On the tokio chat, @carllerche suggested that one way to simplify the "errors on streams" problem (rust-lang/futures-rs#206) would be to remove the error alternative from Future & Stream. This idea stuck with me, and I want to sketch out a possible alternative, to see how it would look.

Currently

  • Future<Item, Error>: A future resolves to either an item or an error type, exactly like an async version of Result.
    • poll() -> Poll<Item, Error>: not ready, or an item, or an error
  • Stream<Item, Error>: A stream emits elements that are each either an item or error, exactly like an async iterable of Result.
    • poll() -> Poll<Option<Item>, Error>: not ready, or the end of the stream (Ready(None)), or an item, or an error

Most streams will be "dead" once they emit an error, so many combinators assume that an error item is also the final possible emitted element, hence the above bug.

Alternative 1

  • Future<Item>: A future is an async value. If the value could fail, the item is a Result. (Future<Result<T, Error>>)
    • poll() -> Poll<Item>: not ready, or a value
  • Stream<Item>: A stream is an async iterator, which just emits values until it's done. Stream<Result<T, Error>> mimics the old behavior.
    • poll() -> Poll<Option<Item>>: not ready, or end of the stream, or an item

Alternative 2

I don't think the streams from alternative 1 are very commom in the wild. Instead, I think they often emit values until they end successfully or encounter an error. So maybe a stream is an async iterator that emits values until it ends, with a Result representing final success or failure.

  • Stream<Item, T>: A stream is an async iterator, emitting values until ending with a final result T. If the stream can end in error, T should be a Result.
    • Stream<Item, Result<(), Error>> is the network case: items arrive until the stream ends or encounters an error.
    • Stream<Item, Result<T, Error>> is a special case where the completed stream has some final value, or would like to return some resource that it used to process the stream (like a socket).

The collect() method would instead return Future<(Iterator<Item>, T)>. It's unwieldly but would allow a process to handle errors.

The into_future() method would similarly need to return a future of an enum: a possible next value, or the final stream result: Future<...> where enum is either NextItem(Item, Stream) or Done(T)

This alternative is more complicated, so I'm not sure how I feel about it. But it does solve the problem that streams usually end because they succeeded or failed, which isn't handled in alternative 1.

@46bit
Copy link

46bit commented Apr 10, 2017

Thanks for this, interesting read.

Just thinking about potential ugliness - in Alternative 2, a Stream which can non-fatally error would be:

  • Only non-fatal errors: Stream<Result<Item, Error>, ()>. Not too bad compared to the existing situation?
  • Non-fatal and fatal errors:Stream<Result<Item, Error>, Result<(), Error>>. Hmm.
  • Non-fatal, fatal, and final value errors:Stream<Result<Item, Error>, Result<T, Error>>. Extreme case, maybe a bit hard to reason about from the type.

I would like if a Stream could have a specified end value, as an async equivalent of into_inner(). I'm not clear what the return values would be for a stream encountering an error and returning a final value?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment