are very e ffi cient • Haskell IO multiplexes all ongoing IO requests using e ff i cient operating system primitives such as epoll on Linux. Thus applications with lots of lightweight threads, all doing IO simultaneously, perform very well
• Concurrency does not make sense for pure expressions Parallelism still does • But you can still perform e ff ects with the IO Monad. e.g. printLn ⸬ String → IO () • E ff ectful values have to be sequenced to have an e ff ect
• printMyValue = printLn “Hello” does not actually print anything • The *only* way to sequence e ff ects is by declaring *one* to be the main e ff ect - • main = printMyValue prints “Hello”!
combine multiple values into a single value • Bind is a combinator. Haskell is big on combinators! • Makes sense that the concurrency APIs are also combinators heavy • No special syntax needed, just function calls • Combinators compose • So concurrent code feels like using lego bricks to build larger programs
set of orthogonal general purpose APIs for concurrency • That you can combine in multiple ways to create the desired functionality. • That means you can build your own models of concurrency, including - • Shared memory • Transactions • Actor models • Etc.
— This call returns immediately getFoo params \fooResult -> — This code will run only when the result is available use fooResult … Meanwhile do something else …
separate thread forkIO :: IO a -> IO ThreadId • Something like getFoo could be implemented using forkIO - getFoo params handler = forkIO do fooResult <- makeAPICall params handler fooResult • Note the monadic sequencing to invoke the handler
So this program - getFoo params \fooResult -> use fooResult meanwhile do something else • Is equivalent to - forkIO do fooResult <- makeAPICall params use fooResult meanwhile do something else
getFoo and getBar together, cancel the other as soon as one returns • Use race :: IO a -> IO b -> IO (Either a b) • Usage res <- race getFoo getBar case res of Left fooResult -> do something with Foo Right barResult -> do something with Bar Sum types FTW!
instance newtype Concurrently a = Concurrently (IO a) <$> :: (a -> b) -> Concurrently a -> Concurrently b <*> :: Concurrently (a -> b) -> Concurrently a -> Concurrently b action :: Concurrently (Foo, Bar, Baz) action = mk3Tuple <$> getFoo <*> getBar <*> getBaz Note: Separated for clarity mk3Tuple = (,,)
multiple actions, but return the result from the fi rst one to complete? • Use the Alternative instance with Concurrently! <|> :: Concurrently a -> Concurrently a -> Concurrently a action = getFoo <|> getBar <|> getBaz Error: can’t match Foo with Bar with Baz
a | Second b | Third c <|> :: Concurrently a -> Concurrently a -> Concurrently a <$> :: (a -> b) -> Concurrently a -> Concurrently b First <$> getFoo :: Concurrently (OneOfThree Foo b c) Second <$> getBar :: Concurrently (OneOfThree a Bar c) Third <$> getBaz :: Concurrently (OneOfThree a b Baz) action :: Concurrently (OneOfThree Foo Bar Baz) action = (First <$> getFoo) <|> (Second <$> getBar) <|> (Third <$> getBaz)
getURL :: String -> Concurrently String • We need to map over a list of URLs, fetching their contents concurrently, and returning all of them in a list mapConcurrently :: (a -> Concurrently b) -> [a] -> Concurrently [b] • Haskellers would recognise that mapConcurrently = traverse • Traverse requires Concurrently to be Applicative, which as we saw it already is traverse getURL [“foo.com", “bar.com", “baz.com"]
the previous slide - traverse getURL [“foo.com", “bar.com", “baz.com"] • This launches 3 threads, and an exception in any of them will kill all the other threads as well. • Using race or concurrently, we are building a tree of threads, where all the threads are always cleaned up. • For now, we will not discuss exceptions further in this presentation
happens less than you think • Note that I said “prebuilt”, not “inbuilt” • All of these functions are written, in user code, upon lower level primitives • The implementations are very simple. Usually you can count the number of lines on one hand. • You can write your own abstractions very easily • But most of the time, you will just reach into your standard Haskell toolbox
getBool concurrently. • If getBool fi nishes fi rst, and is true, then answer with 100, cancelling getInt • Else wait for and return the result of getInt • Since we need to inspect the result of one action before we decide whether to cancel the other action, this is not doable with race and concurrently
newMVar threadBool <- forkIO (getBool >>= boolHandler v) threadInt <- forkIO (getInt >>= intHandler v) takeMVar v where boolHandler b = when b do cancelThread threadInt putMVar v 100 intHandler i = putMVar v i
(MVar a) ThreadId async :: IO a -> IO (Promise a) async action = do var <- newMVar tid <- forkIO (action >>= putMVar var) return (Promise var tid) await :: Promise a -> IO a await (Promise var _) = takeMVar var cancel :: Promise a -> IO () cancel (Promise _ tid) = cancelThread tid
async getBool intPromise <- async getInt v <- race (Left <$> await boolPromise) (Right <$> await intPromise) case v of Left True -> do cancel intPromise pure 100 Left False -> await intPromise Right i -> pure i
Promise (return a) ??? Promise v tid >>= f = Promise ??? • We need a ThreadId, but don’t have one • We need to construct a new MVar for synchronisation but don’t have an IO context Remember, (>>=) :: Promise a -> (a -> Promise b) -> Promise b
:: IO (IO a, IO ()) } async :: IO a -> IO (Promise a) async action = do var <- newMVar tid <- forkIO (action >>= putMVar var) return (Promise (return (takeMVar var, cancelThread tid))) await :: Promise a -> IO a await p = do (take, _) <- runPromise p take cancel :: Promise a -> IO () cancel p = do (_, cancel) <- runPromise p cancel
Shared List of URL contents • We have a producer that adds URLs to the IN list • Multiple consumers that take URLs from the IN list, make network requests, and push the contents to the OUT list