Futures in
Rust and Haskell
Bhargav Voleti and Matthew Wraith
The Languages
Rust
- Systems programming language from Mozilla
- Focus on memory safety and performance
- Ripgrep, servo, aws firecracker, and many backend services.
Rust Syntax
use std::fmt::Debug;
trait HasArea {
fn area(&self) -> f64;
}
impl HasArea for Rectangle {
fn area(&self) -> f64 { self.length * self.height }
}
#[derive(Debug)]
struct Rectangle { length: f64, height: f64 }
// The generic `T` must implement `Debug`. Regardless
// of the type, this will work properly.
fn print_debug<T: Debug>(t: &T) {
println!("{:?}", t);
}
// `T` must implement `HasArea`. Any function which meets
// the bound can access `HasArea`'s function `area`.
fn area<T: HasArea>(t: &T) -> f64 { t.area() }
fn main() {
let rectangle = Rectangle { length: 3.0, height: 4.0 };
print_debug(&rectangle);
println!("Area: {}", area(&rectangle));
}
Rust Ownership
- Provides memory safety
- Each value can only have one binding at a time.
- Once binding goes out of scope, value is dropped.
- At any given time, you can have either one mutable reference or any number of immutable references.
- References must always be valid.
Haskell
- Statically typed, lazy, purely functional programming language designed by committee in the early 90s
- Many compilers that implement the Haskell standard, but GHC is the most popular
- Focuses on correctness and expressivity but still fast
- https://haskell.org
Haskell Syntax
class HasArea a where
area :: a -> Double
instance HasArea Rectangle where
area r = length r * height r
data Rectangle = Rectangle
{ length :: Double
, height :: Double
} deriving Show
scaleRectangle :: Double -> Rectangle -> Rectangle
scaleRectangle s (Rectangle l h) = Rectangle (s * l) (s * h)
debugArea :: (Show a, HasArea a) => a -> IO ()
debugArea a = do
print a
putStrLn ("Area: " <> show (area a))
main :: IO ()
main = do
let rectangle = Rectangle { length = 3.0, height = 4.0 }
debugArea rectangle
-- "Mappable"
class Functor f where
fmap :: (a -> b) -> f a -> f b
instance Functor [] where
fmap :: (a -> b) -> [a] -> [b]
instance Functor (Map k) where
fmap :: (a -> b) -> Map k a -> Map k b
instance Functor IO where
fmap :: (a -> b) -> IO a -> IO b
-- Mapping +1 over a list
fmap (+ 1) [1,2,3] == [2,3,4]
readFile :: FilePath -> IO String
-- Read the file and append !!! to the result
fmap (<> "!!!") (readFile file) :: IO String
Functor, aka Mappable
-- "Mappable"
class Functor f where
fmap :: (a -> b) -> f a -> f b
-- "AndThenable"
class Functor f => Monad f where
return :: a -> f a
(>>=) :: f a -> (a -> f b) -> f b
instance Monad Maybe where
return :: a -> Maybe a
(>>=) :: Maybe a -> (a -> Maybe b) -> Maybe b
instance Monad [] where
return :: a -> [a]
(>>=) :: [a] -> (a -> [b]) -> [b]
instance Monad IO
return :: a -> IO a
(>>=) :: IO a -> (a -> IO b) -> IO b
What the hell are monads?
readFile :: FilePath -> IO String
print :: Show a => a -> IO ()
-- >>= for IO
(>>=) :: IO a -> (a -> IO b) -> IO b
-- Read a file then do another IO action
main :: IO ()
main = readFile file >>= print
-- Bind to a name, concat results
[1,2,3] >>= \x -> [x + x, x * x] == [2,1,4,4,6,9]
[0,1,2,3] >>= \x -> [2 * x, 2 * x + 1] == [0,1,2,3,4,5,6,7]
-- Works just as well for Maybe (aka, Option)
lookup :: k -> Map k v -> Maybe v
lookup "x" m >>= \x -> return (x + x)
-- Read the file, append !!!, bind to "contents",
-- print contents appended to itself.
-- If the file reads "hello" the output to stdout will be "hello!!! hello!!!"
fmap (<> "!!!") (readFile file) >>= \contents ->
print (contents <> " " <> contents)
The M-word
fn friends_in_common(
name1: Name,
name2: Name,
people: Map<Name, Set<Friends>>,
) -> Option<Set<Friends>> {
match people.lookup(name1) {
Some(friendSet1) =>
match people.lookup(name2) {
Some(friendSet2) => friendSet1.intersection(friendSet2),
None => None,
}
None => None,
}
}
>>= == and_then
lookup :: k -> Map k v -> Maybe v
friendsInCommon :: Name -> Name
-> Map Name (Set Friends)
-> Maybe (Set Friends)
friendsInCommon name1 name2 people =
case lookup name1 people of
Just friendSet1 ->
case lookup name2 people of
Just friendSet2 -> Just (intersection friendSet1 friendSet2)
Nothing -> Nothing
Nothing -> Nothing
fn friends_in_common(
name1: Name,
name2: Name,
people: Map<Name, Set<Friends>>,
) -> Option<Set<Friends>> {
people.lookup(name1).and_then(|friendSet1| {
people.lookup(name2).and_then(|friendSet2| {
friendSet1.intersection(friendSet2))
})
})
}
>>= == and_then
lookup :: k -> Map k v -> Maybe v
friendsInCommon :: Name -> Name
-> Map Name (Set Friends)
-> Maybe (Set Friends)
friendsInCommon name1 name2 people = do
friendSet1 <- lookup name1 people
friendSet2 <- lookup name2 people
return (intersection friendSet1 friendSet2)
friendsInCommon name1 name2 people =
lookup name1 people >>= (\friendSet1 ->
lookup name2 people >>= (\friendSet2 ->
Just (intersection friendSet1 friendSet2)))
-- Read a file then do another IO action
main :: IO ()
main = do
contents <- readFile file
print contents
-- Bind to a name, concat results
do
x <- [1,2,3]
[x + x, x * x]
-- Works just as well for Maybe (aka, Option)
do
x <- lookup "x" m
return (x + x)
-- If the file reads "hello" the output to stdout will be "hello!!!hello!!!"
do
contents <- fmap (<> "!!!") (readFile file)
print (contents <> contents)
return contents
The M-word
The Idea
What's is a future?
A value which represents the completion of an async computation.
- Database queries
- An RPC invocation
- A timeout
- An external API request.
- Offload long running CPU-intensive task
- Performing I/O operations
How do they work
- The future simply represents the async computation.
- The computation itself is generally performed by an event loop.
- Cooperative multitasking vs Preemptive multitasking.
- DO NOT BLOCK.
Why would you want to use them
- High performance evented systems.
- Threaded model has lots of overhead when running lots of small tasks.
- GUIs
The Architecture
Rust
Rust
Execution model
You create a future
let response = client.get("http://httpbin.org");
You set it up to use the value after it has been computed.
let response_is_ok = response
.and_then(|resp| {
println!("Status: {}", resp.status());
Ok(())
});
You actually run it to perform the computation
tokio::run(response_is_ok);
Haskell
- Async is a futures library, and io-streams is a streaming library.
- Focus on a small and simple API with lots of expressiveness and obviously correct constructions.
- Can call wait or poll on Asyncs.
- io-streams have InputStreams, for reading from, and OutputStreams for writing to.
Execution model
do
response <- async (get client "http://httpbin.org")
responseIsOkay <- wait (fmap (\resp -> isOk (status resp)) response)
putStrLn ("Got: " <> show responseIsOkay)
async creates an Async data type that can be polled or waited upon.
STM (briefly)
- Like database transactions for memory
- Atomic operations on memory
- Log state before transaction, rollback if needed
- Previous STM talk:
https://slides.com/wraithm/deck
The Differences
- Haskell uses a heavy run-time system
- Haskell has lightweight threads that map to OS threads.
- Futures in Rust have a Stream type. IO-streams is a separate library in Haskell.
- >>= is exactly the same as and_then in rust. But >>= works for sequencing any IO.
The libraries
THE TYPES
pub enum Async<T> {
Ready(T),
NotReady,
}
trait Future {
/// The type of the value returned when the future completes.
type Item;
/// The type representing errors that occurred while
/// processing the computation.
type Error;
/// The function that will be repeatedly called to see if the future
/// has completed or not. The `Async` enum can either be `Ready` or
/// `NotReady` and indicates whether the future is ready to produce
/// a value or not.
fn poll(&mut self) -> Result<Async<Self::Item>, Self::Error>;
}
Rust
Defined in the Futures library
Rust
WORKING WITH FUTURES
/// Change the type of the result of the future.
fn map<F, U>(self, f: F) -> Map<Self, F>
where
F: FnOnce(Self::Item) -> U,
Self: Sized
/// This will call the passed in closure with the result of
/// the future, only if it was successful.
fn and_then<F, B>(self, f: F) -> AndThen<Self, B, F>
where
F: FnOnce(Self::Item) -> B,
B: IntoFuture<Error = Self::Error>,
Self: Sized,
/// Waits for either one of two futures to complete.
fn select<B>(self, other: B) -> Select<Self, B::Future>
where
B: IntoFuture<Item = Self::Item, Error = Self::Error>,
Self: Sized,
/// And many more: https://docs.rs/futures/0.1.25/futures/future/trait.Future.html
Provides a lot of adapters similar to iter().
Rust
STREAMS OF VALUES
- Two parts:
- Stream
- Think of it as an iterator of futures.
- Represents 3 states:
-
Ok(Some(value)) -> got value
- Ok(None) -> Stream closed
- Err(err) -> some error occurred
- Sink
- As the name suggests, this future simply writes to underlying IO
- Provides back pressure via `start_send`
Rust
Tokio
From the Tokio docs, Tokio is a:
- A multi threaded, work-stealing based task scheduler.
- A reactor backed by the operating system's event queue (epoll, kqueue, IOCP, etc...).
- Asynchronous TCP and UDP sockets.
- Asynchronous filesystem operations.
- Timer API for scheduling work in the future.
Rust
Tokio
- The thing that runs your futures.
- Provides types for working with sockets and I/O.
/// Creates a new executor and spawns the future on it and
/// runs it to completion.
pub fn run<F>(future: F)
where
F: Future<Item = (), Error = ()> + Send + 'static,
/// Spawns the given future on the executor.
pub fn spawn<F>(f: F) -> Spawn
where
F: Future<Item = (), Error = ()> + 'static + Send,
/// There is a separate API for futures which don't have to be `Send`
Async API
data Async a
instance Functor Async
class Functor f where
fmap :: (a -> b) -> f a -> f b
-- fmap :: (a -> b) -> Async a -> Async b
-- (>>=) :: IO a -> (a -> IO b) -> IO b
async :: IO a -> IO (Async a)
wait :: Async a -> IO a
Async API
data Async a
instance Functor Async
async :: IO a -> IO (Async a)
wait :: Async a -> IO a
poll :: Async a -> IO (Maybe (Either SomeException a))
waitEither :: Async a -> Async b -> IO (Either a b)
waitBoth :: Async a -> Async b -> IO (a, b)
waitAny :: [Async a] -> IO (Async a, a)
mapConcurrently :: Traversable t => (a -> IO b) -> t a -> IO (t b)
Async Implementation
data Async a = Async
{ asyncThreadId :: ThreadId
, asyncWait :: STM (Either SomeException a)
}
-- atomically :: STM a -> IO a
-- run 'asyncWait', ie. read from TMVar, and either throw or return
wait :: Async a -> IO a
wait a = atomically (asyncWait a >>= either throw return)
-- Do your action on another thread and put the result in TMVar,
-- waiting on an async is just reading from TMVar
async :: IO a -> IO (Async a)
async action = do
var <- atomically newEmptyTMVar
t <- forkFinally action (\r -> atomically (putTMVar var r))
return (Async t (readTMVar var))
Async Implementation
data Async a = Async
{ asyncThreadId :: ThreadId
, asyncWait :: STM (Either SomeException a)
}
-- atomically :: STM a -> IO a
-- If the first action completes without retrying then it forms
-- the result of the orElse. Otherwise, if the first action retries,
-- then the second action is tried in its place. If both fail, repeat.
-- orElse :: STM a -> STM a -> STM a
poll :: Async a -> IO (Maybe (Either SomeException a))
poll a = atomically ((fmap Just (asyncWait a)) `orElse` return Nothing)
waitEither :: Async a -> Async b -> IO (Either a b)
waitEither left right =
atomically (
(fmap Left (waitSTM left))
`orElse`
(fmap Right (waitSTM right))
)
Haskell
io-streams
data InputStream a
data OutputStream a
-- Read from InputStreams
read :: InputStream a -> IO (Maybe a)
-- Write to OutputStreams, Nothing is end of stream
write :: Maybe a -> OutputStream a -> IO ()
-- Take InputStream and pipe to OutputStream
connect :: InputStream a -> OutputStream a -> IO ()
-- Merge all input streams
concurrentMerge :: [InputStream a] -> IO (InputStream a)
-- Ends of concurrent queue
makeChanPipe :: IO (InputStream a, OutputStream a)
The Code!
Rust
Simple IO
TO THE CODE!
Rust
Simple IO - Redux
use tokio::io::{copy, stdout};
use futures::Future;
fn main() {
let task = tokio::fs::File::open("/Users/bigb/vimwiki/index.md")
.and_then(|file| {
// do something with the file ...
copy(file, stdout())
})
.and_then(|(n, _, _)| {
println!("Printed {} bytes", n);
Ok(())
})
.map_err(|e| {
// handle errors
eprintln!("IO error: {:?}", e);
});
tokio::run(task);
}
Haskell
Simple IO
-- Print all of the file, then traverse the contents again to
-- get the number of bytes
simpleFileDump :: FilePath -> IO ()
simpleFileDump file = do
contents <- openFile file
print contents
putStrLn ("Printed " <> show (length contents) <> " bytes")
-- Count all the bytes that are consumed by a stream
countInput :: InputStream ByteString -> IO (InputStream ByteString, IO Int64)
-- Open a file as a stream and close it when done
withFileAsInput :: FilePath -> (InputStream ByteString -> IO a) -> IO a
-- Stream byte by byte into stdout, counting
streamingFileDump :: FilePath -> IO ()
streamingFileDump file = do
numBytes <- withFileAsInput file (\fileStream -> do
(countedStream, getBytes) <- countBytes fileStream
connect countedStream stdout
getBytes)
putStrLn ("Printed " <> show numBytes <> " bytes")
Rust
World's dumbest HTTP client
Haskell
World's dumbest HTTP client
getUrl :: URL -> IO Response
status :: Response -> StatusCode
mapConcurrently :: Traversable t => (a -> IO b) -> t a -> IO (t b)
-- when t is a list:
mapConcurrently :: (a -> IO b) -> [a] -> IO [b]
-- Concurrently get the responses from various urls
-- and then print the status codes
mapConcurrently getUrl urls >>= traverse (print . status)
A Chat server!
BACK TO THE CODE!
The End
Questions?
careers@bitnomial.com
Async IO in Rust and Haskell
By wraithm
Async IO in Rust and Haskell
- 5,512