Quick and Easy Concurrency with Haskell

Jonathan Fischoff
May 18, 2017 · 2 min read

I love having an excuse to do concurrent programming in Haskell. Along with refactoring, it is one of its superpowers.

A common pattern in interactive programming is having a slow consumer (input processing) and a fast producer (user input).

Imagine you are making…oh I don’t know…a hotdog recognizer.

Sure, I know what you are thinking — there already plenty of those — but ours will be different: it will be terminal based.

Terminal hotdogs are the tastiest

Requirements

If a user is holding ‘h’ then after the analyzer runs, it should classify the input as a “terminal hotdog”. If the user is holding down anything else it should classify it as “not a terminal hotdog”.

The Rub

The problem is that our classifier is not done yet, so we are going to make it appear like it is working for a demo (it will sleep for 4 seconds). We don’t want to queue up old classification results (key requirement). Instead, the app should attempt show the classification result for the most recent input.

We need a bounded queue of size 1. A single element queue is another way to view an MVar.

The consumer thread that does the classification will read from theMVar if there is a new element, and will block if the MVaris empty.

import Control.Concurrent
import Control.Monad
consumer :: MVar Char -> IO ()
consumer var = forever $ do
-- Read the most recent character.
-- Block and wait for a character if the MVar is empty.
x <- takeMVar var
putStrLn "\nanalyzing"
-- Sleep for 4 secs to trick our investors
threadDelay 4000000

-- TODO actual use tensorflow for real here with this
putStrLn ""
if x == 'h' then
putStrLn "It's a terminal hotdog"
else
putStrLn "It's not a terminal hotdog"

The producer will wait for input from the user. When it gets a character, it will clear the MVar and insert the most recent value.

producer :: MVar Char -> IO ()
producer var = forever $ do
-- Wait for a user to input a character
input <- getChar
-- Empty the MVar if there is older input waiting for processing.
_ <- tryTakeMVar var
-- Insert the new into the MVar so the consumer can process it.
putMVar var input

Tie it Together

In the main function, we create a new empty MVar and pass it new a thread for the consumer. We call the producer on the main thread.

main :: IO ()
main = do
var <- newEmptyMVar
forkIO $ consumer var
producer var

The great thing about this is, it works, it’s easy and this patterns shows up a lot, so it’s worth learning.