thma/PolysemyCleanArchitecture

Change the server in interpreter?

stevemao opened this issue · 3 comments

Hi there, thanks for the write up.

I'm wondering if we want to change the server to serverless, is it easy to do that? If we treat the server is an effect, can we just change the interpreter?

thma commented

Hi Steve,

That's a cool idea!

There is https://hackage.haskell.org/package/servant-polysemy which allows to run Servant as a polysemy effect. here is a very simple example:

https://github.com/AJChapman/servant-polysemy/blob/master/example/Server.hs

At the moment I don't have the time to do it. But all Pull requests are welcome!

thma commented

Hi again Steve,

I came up with the following solution:

  1. Define a new ExternalInterfaces.AppServer effect

  2. Provide a Warp based implementation ExternalInterfaces.WarpAppServer

  3. write a main function that runs the whole Polysemy application
    here is the code:

  4. AppServer.hs:

{-# LANGUAGE TemplateHaskell #-}
module ExternalInterfaces.AppServer where

import Polysemy ( makeSem )
import Network.Wai (Application)
import InterfaceAdapters.Config


data AppServer m a where
  ServeApp           :: Int -> Application -> AppServer m ()
  ServeAppFromConfig :: Config -> AppServer m ()

makeSem ''AppServer
  1. WarpAppServer.hs:
module ExternalInterfaces.WarpAppServer where

import           ExternalInterfaces.AppServer
import           Polysemy (Embed, Sem, Member, embed, interpret, runM )
import qualified Network.Wai.Handler.Warp as Warp (run)
import           InterfaceAdapters.Config                 (Config(..))
import           ExternalInterfaces.ApplicationAssembly (createApp, loadConfig)

    
-- | Warp Based implementation of AppServer
runWarpAppServer :: (Member (Embed IO) r) => Sem (AppServer : r) a -> Sem r a
runWarpAppServer = interpret $ \case
  ServeApp port app         -> embed $ Warp.run port app
  ServeAppFromConfig config -> embed $ 
        let p   = port config
            app = createApp config
        in  Warp.run p app
  1. Main.hs:
-- | in this example the AppServer effect is interpreted by runAppServerOnWarp
warpAsEffectMain :: IO ()
warpAsEffectMain = do
  config <- loadConfig
  serveAppFromConfig config
    & runWarpAppServer    -- use Warp to run rest application
    & runM

What I like about this approach is that the polysemy effect interpretation now remains the outermost layer of the application.
Everything else, including the Warp server is now just an effect.
That makes the whole architecture still a bit cleaner!

Does this approach match your idea?

thma commented

I put together a blog post that describes my solution:
https://thma.github.io/posts/2022-07-04-polysemy-and-warp.html