Haskell roguelike - Structure

Posted on April 2, 2018

start prev next

Code structure

Even with everything we have implemented so far the game code is still fairly small somewhere around 1400 lines of Haskell. Its also still pretty manageable with the basic structure of engine, core, levels and hosting. But as your code base starts getting more complex you may feel that you need a bit more structure.

Here I’m very briefly going to look at implementing Matt Parsons’ Three Layer Haskell Cake.

This is entirely optional and I’m certainly not even going to attempt explain things better than Matt already has. So if any of this does not make sense feel free to rather skip to the next chapter

Identifying the layers

  1. Layer 1 / orchestration: The code that initialises the client and loops waiting for commands from the client
  2. Layer 2 / external services: The external service is the web socket connection
  3. Layer 3 / pure functional: Everything else

The majority of the code is already fairly basic and pure, which is good news. Most of the functionality already fits nicely into layer 3. All the code that deals with the TVar will be moved into layer 1 or 2.

I’ve not changed the hosting code to fit into the layers, so I’ve just called it layer 0. I could have made it part of layer 1, either by making a HostT or alternatively by having Apt store a Maybe World and Connection. If my hosting code got any more complicated I’d do something like that.

Layer 0

All the code in GameHost is layer zero as is runGame and manageConnection from GameEngine. manageConnection starts the AppT pipeline (initialise and startGame)

21_console/src/GameEngine.hs (41 to 69)
----------------------------------------------------------------------------------------------------------------
-- L0 Hosting
----------------------------------------------------------------------------------------------------------------
runGame :: (Levels -> Level) -> IO ()
runGame getLevel = 
  Host.runHost $ manageConnection getLevel


manageConnection :: (Levels -> Level) -> Host.Connection -> IO ()
manageConnection getLevel conn = do
  initCmd <- conn ^. conReceiveText 

  case parseCommand initCmd of
    Just ("init", cmdData) -> do
      std <- Rnd.getStdGen
      
      case initialiseConnection conn cmdData std getLevel of -- initialiseConnection is L3
        Right world -> do
          app <- atomically $ AppState <$> newTVar conn <*> newTVar world

          -- Start the AppT pipeline
          runReaderT (unAppT $ initialiseConfig >> startGame) app

        Left e ->
          runReaderT (sendHostError e) conn
        
    _ ->
      pass
----------------------------------------------------------------------------------------------------------------

Layer 1

Layer 1 defines AppState and AppT. It has the initialiseConfig and startGame functions. startGame gets commands from the web sockets and starts layer two’s handleCommand

21_console/src/GameEngine.hs (74 to 115)
----------------------------------------------------------------------------------------------------------------
-- L1 App (orchestration)
----------------------------------------------------------------------------------------------------------------
data AppState = AppState { appConnection :: !(TVar Host.Connection)
                         , appWorld :: !(TVar World)
                         } 

newtype AppT m a = AppT { unAppT :: ReaderT AppState m a 
                        } deriving (Functor, Applicative, Monad, MonadReader AppState, MonadTrans)

askApp :: (AppState -> TVar a) -> AppT IO a
askApp getter = do
  app <- ask
  lift . atomically . readTVar $ getter app

modifyApp :: (AppState -> TVar a) -> (a -> a) -> AppT IO ()
modifyApp getter modify' = do
  app <- ask
  lift . atomically $ modifyTVar' (getter app) modify'
----

initialiseConfig :: AppT IO ()
initialiseConfig = do
  conn <- askApp appConnection
  world <- askApp appWorld
  app <- ask
  lift $ runReaderT (sendConfig $ world ^. wdConfig) (conn, appWorld app)
  

startGame :: AppT IO ()
startGame = do
  app <- ask
  conn <- askApp appConnection

  lift . forever $ do
    t <- conn ^. conReceiveText
    r <- runReaderT (handleCommand t) (conn, appWorld app)

    case r of
      Nothing -> pass
      Just e -> putText e
----------------------------------------------------------------------------------------------------------------

Layer 2

Layer 2 acts as a bridge to external services. The only external service here is the web socket connection. The MonadHost class abstracts actions on this connection. There are two instances of the type class. One for the normal case where we have the connection and a World and one for when we only have a connection. I could have chosen to use a Maybe World and only have one instance but I think this was is clearer.

Since there are two instances and they share most of their code, the type class has a default implementation for most of the functions.

21_console/src/GameEngine.hs (120 to 178)
----------------------------------------------------------------------------------------------------------------
-- L2 (bridge / external services)
----------------------------------------------------------------------------------------------------------------
class (Monad m) => MonadHost m where
  receiveHostText :: m Text
  sendHostData :: Text -> m ()
  sendHostLog :: Text -> m ()
  sendHostError :: Text -> m ()
  sendHostUiConfig :: UiConfigData -> m ()
  compressData :: Text -> m BSL.ByteString

  --default implementation
  sendHostLog err = sendHostData $ Ae.encodeText $ UiMessage "log" err
  sendHostError err = sendHostData $ Ae.encodeText $ UiMessage "error" err
  sendHostUiConfig config = sendHostData . Ae.encodeText $ UiConfig "config" config
  compressData = pure . Bz.compress . BSL.fromStrict . TxtE.encodeUtf8 

instance MonadHost (ReaderT (Host.Connection, TVar World) IO) where
  receiveHostText = do
    (conn, _) <- ask
    lift $ conn ^. conReceiveText

  sendHostData t = do
    (conn, _) <- ask
    lz <- compressData t
    lift $ conn ^. conSendData $ lz

instance MonadHost (ReaderT Host.Connection IO) where
  receiveHostText = do
    conn <- ask
    lift $ conn ^. conReceiveText

  sendHostData t = do
    conn <- ask
    lz <- compressData t
    lift $ conn ^. conSendData $ lz


class (Monad m) => MonadWorld m where
  askWorld :: m World
  putWorld :: World -> m ()
  modifyWorld :: (World -> World) -> m ()
  debugPrint :: Text -> m ()

instance MonadWorld (ReaderT (Host.Connection, TVar World) IO) where
  askWorld = do
    (_, wt) <- ask
    w <- lift . atomically $ readTVar wt
    pure w
    
  putWorld w = do
    (_, wt) <- ask
    lift . atomically $ writeTVar wt w

  modifyWorld fn = do
    (_, wt) <- ask
    lift . atomically $ modifyTVar' wt fn
  
  debugPrint = putText

Layer 2 has handleCommand, runCmd and sendConfig. The functions that need to deal with the external service and call functions from level 3.

21_console/src/GameEngine.hs (183 to 266)
handleCommand :: (MonadHost m, MonadWorld m) => Text -> m (Maybe Text)
handleCommand t = 
  case parseCommand t of
    Nothing -> pure . Just $ "error parsing: " <> t
    Just (cmd, cmdData) -> do
      runCmd cmd cmdData
      pure Nothing


runCmd :: (MonadHost m, MonadWorld m) => Text -> [Text] -> m ()
runCmd cmd cmdData = 
  case cmd of
    "redraw" -> 
      case parseScreenSize cmdData of
        Nothing -> sendHostError "missing / invalid screen size"
        Just (sx, sy) -> do
          updatePlayer (plScreenSize .~ (sx, sy))
          world <- askWorld
          sendHostData $ Ae.encodeText (drawAndSend world)
          sendHostLog "draw"
      
    "key" -> do
      -- Handle the key press
      modifyWorld (\w ->
                     -- Do the actions as if they will succeed
                     let pendingWorld = runActions w $ handleKey w cmdData in
                     -- Apply, if the move is allowed
                     -- Cost is hard-coded to 100 for now, this will be fixed later
                     playerMoving 100 pendingWorld w
                  )

      -- Get the updated world
      w2 <- askWorld

      -- Handle the annotations
      -- This is not terribly pretty as its doing a select for update, but its good enough for debugging
      -- the annotation code can be removed once everything is working
      let annotations = w2 ^. wdUtilBrainAnnotations 
      modifyWorld (\w -> w & wdUtilBrainAnnotations .~ [])
      --printAnnotations annotations

      -- Draw
      w3 <- askWorld
      sendHostData $ Ae.encodeText (drawAndSend w3)

    _ ->
      sendHostError $ "Unknown command: " <> cmd

  where
    updatePlayer f = modifyWorld (\w -> w & wdPlayer %~ f)

    printAnnotations as = do
      debugPrint ""
      debugPrint ""
      debugPrint ""
      debugPrint ""
      debugPrint "***** Utility Annotations **************"
      traverse_ printAnnotation as
      debugPrint "****************************************"
      debugPrint ""

    printAnnotation (e, assess, top)  = do
      debugPrint ""
      debugPrint $ "-----------------------" <> show e
      debugPrint "  -- assess --"
      debugPrint . Txt.intercalate "\n" $ showEntries <$> assess
      debugPrint ""
      debugPrint "  -- top --"
      debugPrint . Txt.intercalate "\n" $ showEntries <$> top
      debugPrint "-----------------------"

    showEntries :: UtilAnnotationEntry -> Text
    showEntries e =
      case e of
        UeAt a -> "    At: " <> a
        UeSelectTopNone n -> "    No utils: " <> n
        UeSelectTopAbove f  -> "    Top above: " <> showF f
        UeSelectTopOne val n i d -> "    Select top one: " <> n <> ", impulse=" <> show i <> ", score=" <> showF val <> "," <> d
        UeNote n -> "    Note: " <> n

  
sendConfig :: (MonadHost m) => Config -> m ()
sendConfig config =
  sendHostData . Ae.encodeText $ UiConfig "config" (buildConfig config)

Level 3

Level 3 has everything else.

Structure done

I think this new structure nicely separates the different concerns. It also makes it clear what should be pure code and its nice to see how much of the code is pure.

Adopting this structure is not required but I think it is worth considering as your game gets more complicated

Chapters

start prev next

Changes

src/GameEngine.hs

diff -w -B -a -d -u -b -r --new-file 19_story/src/GameEngine.hs 20_structure/src/GameEngine.hs
--- 19_story/src/GameEngine.hs
+++ 20_structure/src/GameEngine.hs
@@ -2,6 +2,8 @@
 {-# LANGUAGE NoImplicitPrelude #-}
 {-# LANGUAGE OverloadedStrings #-}
 {-# LANGUAGE MultiWayIf #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
 
 module GameEngine where
 
@@ -20,8 +22,10 @@
 import qualified System.Random as Rnd
 import           Control.Lens (at, _1, (^.), (.~), (%~))
 import qualified Control.Arrow as Ar
+import           Control.Monad.Trans (lift)
+import           Control.Monad.Reader (ask, runReaderT, ReaderT, MonadReader, MonadTrans)
 import           Control.Monad.Writer.Strict (runWriter)
-import           Control.Concurrent.STM (atomically, readTVar, newTVar, modifyTVar', TVar)
+import           Control.Concurrent.STM (atomically, readTVar, writeTVar, newTVar, modifyTVar', TVar)
 
 import qualified Memory as M
 import           GameCore
@@ -33,8 +37,13 @@
 import qualified UtilityBrain as UB
 
 
+
+----------------------------------------------------------------------------------------------------------------
+-- L0 Hosting
+----------------------------------------------------------------------------------------------------------------
 runGame :: (Levels -> Level) -> IO ()
-runGame getLevel = Host.runHost (manageConnection getLevel)
+runGame getLevel = 
+  Host.runHost $ manageConnection getLevel
 
       
 manageConnection :: (Levels -> Level) -> Host.Connection -> IO ()
@@ -45,26 +54,223 @@
     Just ("init", cmdData) -> do
       std <- Rnd.getStdGen
       
-      case initialiseConnection conn cmdData std getLevel of
+      case initialiseConnection conn cmdData std getLevel of -- initialiseConnection is L3
         Right world -> do
-          worldV <- atomically $ newTVar world
-          sendConfig conn $ world ^. wdConfig
-          runConnection worldV
+          app <- atomically $ AppState <$> newTVar conn <*> newTVar world
+
+          -- Start the AppT pipeline
+          runReaderT (unAppT $ initialiseConfig >> startGame) app
+
         Left e ->
-          sendError conn e
+          runReaderT (sendHostError e) conn
         
     _ ->
       pass
+----------------------------------------------------------------------------------------------------------------
 
-  where
-    runConnection worldV = 
-      forever $ do
+
+
+
+----------------------------------------------------------------------------------------------------------------
+-- L1 App (orchestration)
+----------------------------------------------------------------------------------------------------------------
+data AppState = AppState { appConnection :: !(TVar Host.Connection)
+                         , appWorld :: !(TVar World)
+                         } 
+
+newtype AppT m a = AppT { unAppT :: ReaderT AppState m a 
+                        } deriving (Functor, Applicative, Monad, MonadReader AppState, MonadTrans)
+
+askApp :: (AppState -> TVar a) -> AppT IO a
+askApp getter = do
+  app <- ask
+  lift . atomically . readTVar $ getter app
+
+modifyApp :: (AppState -> TVar a) -> (a -> a) -> AppT IO ()
+modifyApp getter modify' = do
+  app <- ask
+  lift . atomically $ modifyTVar' (getter app) modify'
+----
+
+initialiseConfig :: AppT IO ()
+initialiseConfig = do
+  conn <- askApp appConnection
+  world <- askApp appWorld
+  app <- ask
+  lift $ runReaderT (sendConfig $ world ^. wdConfig) (conn, appWorld app)
+  
+
+startGame :: AppT IO ()
+startGame = do
+  app <- ask
+  conn <- askApp appConnection
+
+  lift . forever $ do
         t <- conn ^. conReceiveText
+    r <- runReaderT (handleCommand t) (conn, appWorld app)
+
+    case r of
+      Nothing -> pass
+      Just e -> putText e
+----------------------------------------------------------------------------------------------------------------
+
+
+  
 
+----------------------------------------------------------------------------------------------------------------
+-- L2 (bridge / external services)
+----------------------------------------------------------------------------------------------------------------
+class (Monad m) => MonadHost m where
+  receiveHostText :: m Text
+  sendHostData :: Text -> m ()
+  sendHostLog :: Text -> m ()
+  sendHostError :: Text -> m ()
+  sendHostUiConfig :: UiConfigData -> m ()
+  compressData :: Text -> m BSL.ByteString
+
+  --default implementation
+  sendHostLog err = sendHostData $ Ae.encodeText $ UiMessage "log" err
+  sendHostError err = sendHostData $ Ae.encodeText $ UiMessage "error" err
+  sendHostUiConfig config = sendHostData . Ae.encodeText $ UiConfig "config" config
+  compressData = pure . Bz.compress . BSL.fromStrict . TxtE.encodeUtf8 
+
+instance MonadHost (ReaderT (Host.Connection, TVar World) IO) where
+  receiveHostText = do
+    (conn, _) <- ask
+    lift $ conn ^. conReceiveText
+
+  sendHostData t = do
+    (conn, _) <- ask
+    lz <- compressData t
+    lift $ conn ^. conSendData $ lz
+
+instance MonadHost (ReaderT Host.Connection IO) where
+  receiveHostText = do
+    conn <- ask
+    lift $ conn ^. conReceiveText
+
+  sendHostData t = do
+    conn <- ask
+    lz <- compressData t
+    lift $ conn ^. conSendData $ lz
+
+
+class (Monad m) => MonadWorld m where
+  askWorld :: m World
+  putWorld :: World -> m ()
+  modifyWorld :: (World -> World) -> m ()
+  debugPrint :: Text -> m ()
+
+instance MonadWorld (ReaderT (Host.Connection, TVar World) IO) where
+  askWorld = do
+    (_, wt) <- ask
+    w <- lift . atomically $ readTVar wt
+    pure w
+    
+  putWorld w = do
+    (_, wt) <- ask
+    lift . atomically $ writeTVar wt w
+
+  modifyWorld fn = do
+    (_, wt) <- ask
+    lift . atomically $ modifyTVar' wt fn
+  
+  debugPrint = putText
+
+-------------------
+
+
+handleCommand :: (MonadHost m, MonadWorld m) => Text -> m (Maybe Text)
+handleCommand t = 
         case parseCommand t of
-          Nothing -> putText $ "error parsing: " <> t
-          Just (cmd, cmdData) -> runCmd conn worldV cmd cmdData
+    Nothing -> pure . Just $ "error parsing: " <> t
+    Just (cmd, cmdData) -> do
+      runCmd cmd cmdData
+      pure Nothing
+
+
+runCmd :: (MonadHost m, MonadWorld m) => Text -> [Text] -> m ()
+runCmd cmd cmdData = 
+  case cmd of
+    "redraw" -> 
+      case parseScreenSize cmdData of
+        Nothing -> sendHostError "missing / invalid screen size"
+        Just (sx, sy) -> do
+          updatePlayer (plScreenSize .~ (sx, sy))
+          world <- askWorld
+          sendHostData $ Ae.encodeText (drawAndSend world)
+          sendHostLog "draw"
 
+    "key" -> do
+      -- Handle the key press
+      modifyWorld (\w ->
+                     -- Do the actions as if they will succeed
+                     let pendingWorld = runActions w $ handleKey w cmdData in
+                     -- Apply, if the move is allowed
+                     -- Cost is hard-coded to 100 for now, this will be fixed later
+                     playerMoving 100 pendingWorld w
+                  )
+
+      -- Get the updated world
+      w2 <- askWorld
+
+      -- Handle the annotations
+      -- This is not terribly pretty as its doing a select for update, but its good enough for debugging
+      -- the annotation code can be removed once everything is working
+      let annotations = w2 ^. wdUtilBrainAnnotations 
+      modifyWorld (\w -> w & wdUtilBrainAnnotations .~ [])
+      printAnnotations annotations
+
+      -- Draw
+      w3 <- askWorld
+      sendHostData $ Ae.encodeText (drawAndSend w3)
+
+    _ ->
+      sendHostError $ "Unknown command: " <> cmd
+
+  where
+    updatePlayer f = modifyWorld (\w -> w & wdPlayer %~ f)
+
+    printAnnotations as = do
+      debugPrint ""
+      debugPrint ""
+      debugPrint ""
+      debugPrint ""
+      debugPrint "***** Utility Annotations **************"
+      traverse_ printAnnotation as
+      debugPrint "****************************************"
+      debugPrint ""
+
+    printAnnotation (e, assess, top)  = do
+      debugPrint ""
+      debugPrint $ "-----------------------" <> show e
+      debugPrint "  -- assess --"
+      debugPrint . Txt.intercalate "\n" $ showEntries <$> assess
+      debugPrint ""
+      debugPrint "  -- top --"
+      debugPrint . Txt.intercalate "\n" $ showEntries <$> top
+      debugPrint "-----------------------"
+
+    showEntries :: UtilAnnotationEntry -> Text
+    showEntries e =
+      case e of
+        UeAt a -> "    At: " <> a
+        UeSelectTopNone n -> "    No utils: " <> n
+        UeSelectTopAbove f  -> "    Top above: " <> showF f
+        UeSelectTopOne val n i d -> "    Select top one: " <> n <> ", impulse=" <> show i <> ", score=" <> showF val <> "," <> d
+        UeNote n -> "    Note: " <> n
+
+  
+sendConfig :: (MonadHost m) => Config -> m ()
+sendConfig config =
+  sendHostData . Ae.encodeText $ UiConfig "config" (buildConfig config)
+
+----------------------------------------------------------------------------------------------------------------
+
+
+----------------------------------------------------------------------------------------------------------------
+-- L3 (Business logic, pure code only)
+----------------------------------------------------------------------------------------------------------------
     parseCommand :: Text -> Maybe (Text, [Text])
     parseCommand t =
       case Txt.splitOn "|" t of
@@ -162,91 +367,6 @@
             }
     
 
-runCmd :: Host.Connection -> TVar World -> Text -> [Text] -> IO ()
-runCmd conn worldV cmd cmdData = 
-  case cmd of
-    "redraw" -> 
-      case parseScreenSize cmdData of
-        Nothing -> sendError conn "missing / invalid screen size"
-        Just (sx, sy) -> do
-          updatePlayer (plScreenSize .~ (sx, sy))
-          w <- atomically $ readTVar worldV
-          drawAndSend w
-          sendLog conn "draw"
-      
-    "key" -> do
-      -- Handle the key press
-      atomically $ modifyTVar' worldV (\w ->
-                                         -- Do the actions as if they will succeed
-                                         let pendingWorld = runActions w $ handleKey w cmdData in
-                                         -- Apply, if the move is allowed
-                                         -- Cost is hard-coded to 100 for now, this will be fixed later
-                                         playerMoving 100 pendingWorld w
-                                      )
-      -- Get the updated world
-      w2 <- atomically $ readTVar worldV
-
-      -- Handle the annotations
-      -- This is not terribly pretty as its doing a select for update, but its good enough for debugging
-      -- the annotation code can be removed once everything is working
-      let annotations = w2 ^. wdUtilBrainAnnotations 
-      atomically $ modifyTVar' worldV (\w -> w & wdUtilBrainAnnotations .~ [])
-      printAnnotations annotations
-
-      -- Draw
-      drawAndSend w2
-
-    _ ->
-      sendError conn $ "Unknown command: " <> cmd
-
-  where
-    updatePlayer f = atomically $ modifyTVar' worldV (\w -> w & wdPlayer %~ f)
-
-    printAnnotations as = do
-      putText ""
-      putText ""
-      putText ""
-      putText ""
-      putText "***** Utility Annotations **************"
-      traverse_ printAnnotation as
-      putText "****************************************"
-      putText ""
-
-    printAnnotation (e, assess, top)  = do
-      putText ""
-      putText $ "-----------------------" <> show e
-      putText "  -- assess --"
-      putText . Txt.intercalate "\n" $ showEntries <$> assess
-      putText ""
-      putText "  -- top --"
-      putText . Txt.intercalate "\n" $ showEntries <$> top
-      putText "-----------------------"
-
-    showEntries :: UtilAnnotationEntry -> Text
-    showEntries e =
-      case e of
-        UeAt a -> "    At: " <> a
-        UeSelectTopNone n -> "    No utils: " <> n
-        UeSelectTopAbove f  -> "    Top above: " <> showF f
-        UeSelectTopOne val n i d -> "    Select top one: " <> n <> ", impulse=" <> show i <> ", score=" <> showF val <> "," <> d
-        UeNote n -> "    Note: " <> n
-
-  
-sendLog :: Host.Connection -> Text -> IO ()
-sendLog conn err =
-  sendData conn $ Ae.encodeText $ UiMessage "log" err
-
-
-sendError :: Host.Connection -> Text -> IO ()
-sendError conn err =
-  sendData conn $ Ae.encodeText $ UiMessage "error" err
-
-
-sendConfig :: Host.Connection -> Config -> IO ()
-sendConfig conn config =
-  sendData conn . Ae.encodeText $ UiConfig "config" (buildConfig config)
-
-
 buildConfig :: Config -> UiConfigData
 buildConfig cfg =
   UiConfigData { udKeys = buildKeys (cfg ^. cfgKeys)
@@ -258,12 +378,6 @@
     buildKey (s, a) = UiKey s a
 
 
-sendData :: Host.Connection -> Text -> IO ()
-sendData conn t = do
-  let lz = Bz.compress . BSL.fromStrict . TxtE.encodeUtf8 $ t
-  conn ^. conSendData $ lz
-
-
 parseScreenSize :: [Text] -> Maybe (Int, Int)
 parseScreenSize cmd = do
   (tx, ty) <- case cmd of
@@ -275,15 +389,14 @@
   pure (x, y)
 
 
-drawAndSend :: World -> IO ()
-drawAndSend world = do
-  let layers = drawTilesForPlayer world (world ^. wdMap) 
+drawAndSend :: World -> UiDrawCommand
+drawAndSend world = 
+  let layers = drawTilesForPlayer world (world ^. wdMap) in
   
-  let cmd = Ae.encodeText UiDrawCommand { drCmd = "draw"
+  UiDrawCommand { drCmd = "draw"
                                         , drScreenWidth = world ^. wdPlayer ^. plScreenSize ^. _1
                                         , drMapData = mkDrawMapData <<$>> (Map.toList <$> layers)
                                         }
-  sendData (world ^. wdPlayer ^. plConn) cmd
 
   where
     mkDrawMapData :: (PlayerPos, Tile) -> (Int, Int, Int)

src/GameHost.hs

diff -w -B -a -d -u -b -r --new-file 19_story/src/GameHost.hs 20_structure/src/GameHost.hs
--- 19_story/src/GameHost.hs
+++ 20_structure/src/GameHost.hs
@@ -1,6 +1,8 @@
 {-# LANGUAGE TemplateHaskell #-}
 {-# LANGUAGE NoImplicitPrelude #-}
 {-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
 
 module GameHost ( runHost
                 , Connection(..)
@@ -18,6 +20,9 @@
 import qualified Network.Wai.Handler.WebSockets as WaiWs
 import qualified Network.WebSockets as WS
 
+----------------------------------------------------------------------------------------------------------------
+-- L0 Hosting
+----------------------------------------------------------------------------------------------------------------
 data Connection = Connection { _conSendData :: BSL.ByteString -> IO ()
                              , _conReceiveText :: IO Text
                              }
@@ -55,11 +61,12 @@
 
   
 wsapp :: (Connection -> IO ()) -> WS.ServerApp
-wsapp startHost pending = do
+wsapp runConnection pending = do
   conn <- WS.acceptRequest pending
   WS.forkPingThread conn 30
 
-  startHost Connection { _conSendData = WS.sendBinaryData conn
+  let h = Connection { _conSendData = WS.sendBinaryData conn
                        , _conReceiveText = WS.receiveData conn
                        }
-
+  runConnection h
+----------------------------------------------------------------------------------------------------------------

Chapters

start prev next