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

  • Separation of types and runtime
  • Focus on performance and a memory footprint.
  • Poll based futures.
    • Won't do anything until they are polled.

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