class: center, middle, title-slide count: false  .less-line-height[ Alejandro Serrano @ ZuriHac 2022 .grey[π¦ @trupill - πββ¬ serras - π¨βπ» Tweag] ] --- # π₯ Overall goal ### .grey[How do we build software with Haskell?] 1. ~~Domain-specific languages
Representing actions and scripts
Property-based testing~~ 2. Communicating over the network
Serialization (without boilerplate)
Error handling
Concurrency across threads --- # π Overall goal ### .grey[Build an interactive card game] 1. Represent the cards and the actions 2. Communicate different clients
PokΓ©mon Trading Card Game
Goal: knock out 6 of your opponent's PokΓ©mon using attacks
--- # π Overall goal ### .grey[Build an interactive card game] 1. Represent the cards and the actions 2. Communicate different clients
PokΓ©mon Trading Card Game
Waaaay too complex for 1.5h!
--- # π² Dice roll
--- # π² Dice roll in the cloud βοΈ
--- # π Our stack
Haskell (of course) `network-simple` for networking `binary` and `aeson` for serialization `stm` for concurrency --- # π² One-person dice roll π’ Client requests a roll of _n_ faces βοΈ Server returns a random value
.smaller[.little-margin-top[ _(mandatory XKCD strip)_ ]] --- # π§ Server-side `network-simple` Let's practice signature-reading skills .code70[ ```haskell serve :: MonadIO m => HostPreference -- ^ Host to bind. -> ServiceName -- ^ Server service port to bind. -> ((Socket, SockAddr) -> IO ()) -- ^ Computation to run in a different thread -- once an incoming connection is accepted. -> m a -- ^ This function never returns. ``` ] --- # π§ Server-side `network-simple` `serve` takes care of: - Listening all the time at the given port - Whenever a new connection request comes
1οΈβ£ accept it,
2οΈβ£ **spawn a new thread**,
3οΈβ£ run the callback. --- # βΏ `go` pattern Once we accept, we go into an (infinite) _loop_ - Usually with a recursive `go` function .code70[ ```haskell diceServer :: IO () diceServer = serve "*" "8080" $ \(skt, _) -> do -- initialization go skt where go :: Socket -> IO () go skt = do -- read and parse request -- send a response go skt -- and over again! ``` ] --- # π§ Server-side `network-simple` Requests and responses are 64-bit numbers .little-margin-top[.code70[ ```haskell go :: Socket -> IO () go skt = do -- read and parse request mayBytes <- recv skt 8 case mayBytes of Nothing -> pure () -- done Just bytes -> do let Right max = decode @Word64 bytes response <- encode <$> randomRIO (0, max) -- send a response send skt response go skt -- and over again! ``` ]] --- # π Recognizing end-of-connection If `recv` returns nothing, we stop the loop ```haskell go skt = do -- read and parse request mayBytes <- recv skt 8 case mayBytes of Nothing -> pure () -- done Just bytes -> do ... go skt -- and over again! ``` --- # π Serialization Conversion to and from `ByteString` - `cereal` for binary encoding - `aeson` for JSON - `avro` for Avro (used in Kafka) - ... --- # π Serialization Users implement instances of some classes - Just one, like `Serialize` in `cereal` - Two, like `ToJSON` and `FromJSON` And then conversion is provided by generic ```haskell encode :: Serialize a {- or -} ToFormat a => a -> ByteString decode :: Serialize a {- or -} FromFormat a => ByteString -> Either Error a ``` --- # πͺ’ Type applications Help the compiler to disambiguate types ```haskell decode @Word64 bytes ``` Other possibility is `ScopedTypeVariables` --- # π² One-person dice roll client ## .grey[π§βπ» Time for practice!] .font50[`serras.github.io/zurihac-workshop`] .code70[ ```haskell connect :: (MonadIO m, MonadMask m) => HostName -- ^ Server hostname or IP address. -> ServiceName -- ^ Server service port. -> ((Socket, SockAddr) -> m r) -- ^ Computation taking the communication -- socket and the server address. -> m r ``` ] --- # π² One-person dice roll client ## .grey[π€ How do I test this?] .font50[`serras.github.io/zurihac-workshop`] .code60[ ```haskell $ cabal repl session2 > import DiceRoll DiceRoll> diceServer ^C -- use Ctrl + C to stop ``` ```haskell $ cabal repl session2 > import DiceRoll DiceRoll> diceClient 6 3 DiceRoll> diceClient 10 9 ``` ] --- # βΌοΈ Exceptions You forget to start the server and... ``` > DiceRoll.diceClient 3 *** Exception: Network.Socket.connect:
: does not exist (Connection refused) ``` ## π± WAAAAAAT???? --- # βΌοΈ Exceptions Haskell's `IO` runtime uses **exceptions** - Network or connection problems - File (or resource) not found - and many more input/output problems Even some built-in features use exceptions - Pattern match failure - `undefined` and `error` --- # βοΈ Dealing with exceptions ~~Use `Control.Exception` from `base`~~ π °οΈ Use `Control.Exception.Safe`
from `safe-exceptions` π ±οΈ Use `UnliftIO.Exception` from `unliftio` ### .grey[All three provide the same API] --- # βοΈ Dealing with exceptions If you want to perform some handling ```haskell catch :: (MonadCatch m, Exception e) => m a -> (e -> m a) -> m a ``` If you want to "purify" the problem ```haskell try :: (MonadCatch m, Exception e) => m a -> m (Either e a) ``` --- # βοΈ Idiomatic usage of `catch` .code70[ ```haskell diceClient :: Word64 -> IO (Maybe Word64) diceClient n = connect "127.0.0.1" "8080" (\(skt, _) -> do ...) `catch` (\(e :: IOException) -> pure Nothing) ``` ] - Use `catch` infix - Use `ScopedTypeVariables` to indicate which exceptions are to be caught --- # π« Exception hierarchy Exception types for a hierarchy - The top is called `SomeException` However, if you want to "swallow" all exceptions, better use .code70[ ```haskell catchAny :: MonadCatch m => m a -> (SomeException -> m a) -> m a tryAny :: MonadCatch m => m a -> m (Either SomeException a) ``` ] --- # π§° Resource management ### .grey[Exception + laziness = π΅βπ«] This makes resource management challenging
(even more than usual) π °οΈ Use `resourcet`
π ±οΈ Use `managed`
β `resource-pool` to handle pools of resources --- # π² Dice roll and increment ### .grey[We want to extend the functionality] How do we (de)serialize command and data? 1. Express them as data types 2. Implement `Serialize` instances --- # π² Dice roll and increment ### .grey[We want to extend the functionality] How do we (de)serialize command and data? 1. Express them as data types ```haskell data Request = DiceRoll { max :: Word64 } | Increment { number :: Word64 } type Response = Word64 ``` --- # π² Dice roll and increment ### .grey[We want to extend the functionality] How do we (de)serialize command and data? 1. Express them as data types 2. ~~Implement `Serialize` instances~~ ## Use automatic deriving! --- # π€ Automatic deriving Compiler writes instances for us - `Eq`, `Show`, and others are built-in - Extensible with the `Generic` mechanism .code70[ ```haskell {-# language DeriveGeneric, DeriveAnyClass #-} data Request = DiceRoll { max :: Word64 } | Increment { number :: Word64 } deriving (Generic, Serialize) ``` ] --- # π§ Updated server-side ```haskell let Right max = decode @Word64 bytes response <- encode <$> randomRIO (1, max) ``` Deserialize with the new type ```haskell let Right req = decode @Request bytes response <- case req of DiceRoll max -> randomRIO (1, max) Increment n -> pure (n + 1) ``` --- # π§ Updated server-side ```haskell let Right max = decode @Word64 bytes response <- encode <$> randomRIO (1, max) ``` Deserialize with the new type ```haskell let Right req = decode bytes response <- case req of DiceRoll max -> randomRIO (1, max) Increment n -> pure (n + 1) ``` The type `Request` can be inferred --- # π¬ Greeting ## .grey[π§βπ» Time for practice!] .font50[`serras.github.io/zurihac-workshop`] Add a new command to say "hi!" to people Make it as flexible as you want: - Name of the person to be greeted - Language - Time of the day --- # βοΈπ² Cloudy Rolly βοΈ Central server which processes requests 1οΈβ£ Player #1 joins and gets a code 2οΈβ£ Player #2 joins the session using that code .grey[ π² Players send a new request for rolling - The server sends the winner to both players ] --- # βοΈπ² Cloudy Rolly .grey[ βοΈ Central server which processes requests 1οΈβ£ Player #1 joins and gets a code 2οΈβ£ Player #2 joins the session using that code ] ## 𧡠Each player runs in a different thread πͺ‘ How to communicate across boundaries? --- # πͺ‘ Communication across threads ~~Use `Control.Concurrent` from `base`~~ Use `stm` (Software Transactional Memory) - Similar concept as in databases - Transactions are isolated and atomic - One cannot read the dirty state of another --- # πͺ‘ Software Transactional Memory One cannot read the dirty state of another ```haskell do v <- newTVarIO 0 replicateM 3 $ async $ -- 3 threads atomically $ do -- tx. boundary n <- readTVar v -- β writeTVar (n + 1) -- ββ ``` The end value of `v` is **guaranteed** to be 3 --- # β»οΈ Server as state machine When a user connects... - If they send `NewGame`, send a `GameCode` - When the other player connects, send `GameStarts` - If they send `JoinGame` and the code - If the code exists, send `GameStarts` - Otherwise, send `GameNotFound` and close - Otherwise, close the connection --- # πͺ‘ Shared state Dictionary (key-value map) - Keys are `GameCode`s - Values are the sockets -- ```haskell crServer = do -- initialize shared state state <- newTVarIO @State Map.empty serve "127.0.0.1" "8080" $ \(skt, _) -> worker state skt worker :: TVar State -> Socket -> IO () ``` --- # π· Cloudy Rolly worker Trick to avoid repeating `state` and `skt` .code70[ ```haskell worker :: TVar State -> Socket -> IO () worker state skt = start where start = do ... newGame = ... joinGame code = ... play = putStrLn "play!" ``` ] --- # π· Cloudy Rolly worker Wait for the first message to come .little-margin-top[.code70[ ```haskell worker state skt = start where start = do Just req <- recvJson skt case req of NewGame -> newGame JoinGame code -> joinGame code ``` ```haskell -- read a line and deserialize recvJson :: (MonadIO m, FromJSON a) => Socket -> m (Maybe a) ``` ] ] --- # π· Cloudy Rolly worker .code60[ ```haskell worker state skt = start where newGame = do -- record ourselves in new code code <- randomCode atomically $ modifyTVar state (Map.insert code [skt]) sendJson skt (GameCode code) -- wait for the other message atomically $ do Just skts <- Map.lookup code <$> readTVar state check (length skts > 1) sendJson skt GameStarts -- go! play ``` ] --- # π Retry transactions .code70[ ```haskell -- wait for the other message atomically $ do Just skts <- Map.lookup code <$> readTVar state check (length skts > 1) ``` ] - `check` aborts the tx. if the condition is `False` - The tx. is only retried when any `TVar` mentioned changes its value ### `TVar` works as a synchronization mechanism --- # π· Cloudy Rolly worker .code60[ ```haskell worker state skt = start where joinGame code = do found <- atomically $ do -- tx. boundary βββββββββββββββββββββββββββββββββ result <- Map.lookup code <$> readTVar state -- β case result of -- β Nothing -> pure False -- β Just skts -> do -- β modifyTVar state $ -- β Map.insertWith (<>) code [skt] -- β pure True -- β -- ββββββββββββββββββββββββββββββββββββββββββββββ if found then sendJson skt GameStarts >> play else sendJson skt GameNotFound ``` ] --- # π· Cloudy Rolly worker .code60[ ```haskell worker state skt = start where joinGame code = do found <- atomically $ do -- tx. boundary βββββββββββββββββββββββββββββββββ result <- Map.lookup code <$> readTVar state -- β case result of -- β Nothing -> pure False -- β Just skts -> do -- β modifyTVar state $ -- β Map.insertWith (<>) code [skt] -- β pure True -- β -- ββββββββββββββββββββββββββββββββββββββββββββββ ``` ] Transaction may include pure computations --- # βοΈπ² Cloudy Rolly ## .grey[π π§βπ» (No more) time for practice!] ### Finish the implementation of the server 1. Change `State` to represent on-going games 2. Use `TVar` as synchronization mechanism 3. Write a single `play` implementation ### Finish the implementation of the clients --- # πͺ‘ STM-enabled data structures Many users `==>` lots of contention - Every `JoinGame` "wakes up" every `NewGame` - But we only care about one code! -- ### More fine-grained data structures .margin-top[ - `stm-containers` brings maps and sets - `stm` includes unbounded queues - `stm-chans` adds bounded queues ] --- # π Summary ### .grey[Lots of practice in the `IO` monad] .margin-top[ - GHC uses exceptions for I/O errors - High-level networking with `network-simple` - Automatic deriving =
serialization without the boilerplate - `stm` for communication across threads ] -- - Spawning and controlling threads with `async` - `MonadIO` and `MonadUnliftIO` --- # πΈ A word from our sponsor ## `leanpub.com/haskell-stdlibs`
More on exceptions and resource management
I/O using streaming
Servers and clients with HTTP
Many other useful libraries in the ecosystem
--- class: center, middle, title-slide # π€© It's been a pleasure ## Enjoy the rest of ZuriHac!