demobot
This commit is contained in:
		
							
								
								
									
										148
									
								
								posts/guides/demobot.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										148
									
								
								posts/guides/demobot.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,148 @@
 | 
			
		||||
---
 | 
			
		||||
title: Functional architecture Pt. 1
 | 
			
		||||
date: 2018-12-25
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
I'm lucky enough to work with Haskell professionally which gives me some view
 | 
			
		||||
to good and maintainable real world architecture. In my opinion, one of the
 | 
			
		||||
biggest contributing factors to how your general architecture is defined, is
 | 
			
		||||
determined by the base application monad stack you are using.
 | 
			
		||||
 | 
			
		||||
Our actual product is mostly in the regular `LoggingT (ReaderT app IO)` base
 | 
			
		||||
monad with whatever style you would imagine with that base monad in place. It's
 | 
			
		||||
not entirely consistent, but close enough.
 | 
			
		||||
 | 
			
		||||
With all the talk about just having `IO`, `ReaderT app IO`, free monads or
 | 
			
		||||
tagless final monads, I thought of trying different styles. For this post I'm
 | 
			
		||||
focusing on the tagless final since it's most interesting for me right now.
 | 
			
		||||
 | 
			
		||||
`IO`
 | 
			
		||||
 | 
			
		||||
:   The most basic style. This is pretty much only suitable for the most basic
 | 
			
		||||
of needs.
 | 
			
		||||
 | 
			
		||||
`ReaderT app IO`
 | 
			
		||||
 | 
			
		||||
:   How we mostly define the base monad. This is a really good way of doing
 | 
			
		||||
things, it gives you a lot of leeway on how you can define the rest of your
 | 
			
		||||
application.
 | 
			
		||||
 | 
			
		||||
`Free monads`
 | 
			
		||||
 | 
			
		||||
:   Free monads are a way of having a small constrained DSL or monad stack for
 | 
			
		||||
defining your application. By constraining the user, you are also reducing the
 | 
			
		||||
area for bugs. There is also some possibility for introspection, but usually
 | 
			
		||||
this isn't a usable feature. Also since free monad applications need the full
 | 
			
		||||
AST, they're quite a bit slower than the other solutions.
 | 
			
		||||
 | 
			
		||||
`Tagless final`
 | 
			
		||||
 | 
			
		||||
:   This is something I'm the least familiar with. If I have understood
 | 
			
		||||
correctly, free monads and tagless final are more or less equivalent solutions
 | 
			
		||||
in their power, but in tagless final you aren't creating the AST anywhere,
 | 
			
		||||
which also means that you aren't paying for it either.
 | 
			
		||||
 | 
			
		||||
That out of the way, I had a small project idea for a bot that's easy to
 | 
			
		||||
contribute to, difficult to make errors and easy to reason about. The project
 | 
			
		||||
is at most a proof-of-concept and most definitely not production quality.
 | 
			
		||||
Still, I hope it's complex enough to showcase the architecture.
 | 
			
		||||
 | 
			
		||||
The full source code is available [at my git repository](https://git.rauhala.info/MasseR/demobot).
 | 
			
		||||
 | 
			
		||||
For the architecture to make sense, let me introduce two different actors: a
 | 
			
		||||
*core contributor* that's familiar with Haskell and a *external contributor*
 | 
			
		||||
that's familiar with programming, not necessarily with Haskell.
 | 
			
		||||
 | 
			
		||||
The repository is split into two parts, the library and the application.
 | 
			
		||||
 | 
			
		||||
The library
 | 
			
		||||
 | 
			
		||||
:   Provides the restricted monad classes (tagless final), extension points and
 | 
			
		||||
the core bot main loop.
 | 
			
		||||
 | 
			
		||||
The application
 | 
			
		||||
 | 
			
		||||
:   Provides the implementation for the tagless final type classes, meaning
 | 
			
		||||
that the application defines how the networking stack is handled, how database
 | 
			
		||||
connectivity is done and so on. It also collects all the extensions for that
 | 
			
		||||
specific application.
 | 
			
		||||
 | 
			
		||||
The *core contributor* is responsible for maintaining the library as well as
 | 
			
		||||
the type class instances for the application type. The *external contributor*
 | 
			
		||||
is responsible for maintaining one or multiple extensions that are restricted
 | 
			
		||||
in their capability and complexity.
 | 
			
		||||
 | 
			
		||||
I'm restricting the capabilities of the monad in the library and extensions,
 | 
			
		||||
meaning that I'm not allowing any IO. For example the networking is handled by
 | 
			
		||||
a single `MonadNetwork` type class. This is the most complex type class in the
 | 
			
		||||
library right now, using type families for defining a specific extension point
 | 
			
		||||
for the messages. This could be something like 'event type' for Flowdock
 | 
			
		||||
messages or 'source channel' for IRC messages.
 | 
			
		||||
 | 
			
		||||
~~~haskell
 | 
			
		||||
data Request meta = Request { content :: Text
 | 
			
		||||
                            , meta    :: meta }
 | 
			
		||||
data Response meta = Response { content :: Text
 | 
			
		||||
                              , meta    :: meta }
 | 
			
		||||
 | 
			
		||||
class Monad m => MonadNetwork m where
 | 
			
		||||
  type Meta m :: *
 | 
			
		||||
  recvMsg :: m (Request (Meta m))
 | 
			
		||||
  putMsg :: Response (Meta m) -> m ()
 | 
			
		||||
~~~
 | 
			
		||||
 | 
			
		||||
Then we have the extension point which is more or less just a `Request -> m (Maybe Response)`. I'm using rank n types here for qualifying the `Meta`
 | 
			
		||||
extension point and forcing the allowed type classes to be a subset of the
 | 
			
		||||
application monad stack, I don't want extension writers to be able to write
 | 
			
		||||
messages to the bot network by themselves.
 | 
			
		||||
 | 
			
		||||
~~~haskell
 | 
			
		||||
data Extension meta =
 | 
			
		||||
  Extension { act :: forall m. (meta ~ Meta m, MonadExtension m) => Request meta -> m (Maybe (Response meta))
 | 
			
		||||
            , name   :: String }
 | 
			
		||||
~~~
 | 
			
		||||
 | 
			
		||||
Last part of the library is the main loop, which is basically a free monad
 | 
			
		||||
(tagless final) waiting for an interpreter. At least in this POC I find this
 | 
			
		||||
style to be really good, it's really simplified, easy to read and hides a lot
 | 
			
		||||
of the complexity, while bringing forth the core algorithm.
 | 
			
		||||
 | 
			
		||||
~~~haskell
 | 
			
		||||
mainLoop :: forall m. (MonadCatch m, MonadBot m) => [Extension (Meta m)] -> m ()
 | 
			
		||||
mainLoop extensions = forever $ catch go handleFail
 | 
			
		||||
  where
 | 
			
		||||
    handleFail :: SomeException -> m ()
 | 
			
		||||
    handleFail e = logError $ tshow e
 | 
			
		||||
    go :: m ()
 | 
			
		||||
    go = do
 | 
			
		||||
      msg <- recvMsg
 | 
			
		||||
      responses <- catMaybes <$> mapM (`act` msg) extensions
 | 
			
		||||
      mapM_ putMsg responses
 | 
			
		||||
~~~
 | 
			
		||||
 | 
			
		||||
Then comes the actual application where we write the effectful interpreters. In
 | 
			
		||||
this POC the interpreter is just a `LoggingT IO a` with the semantics of
 | 
			
		||||
stdin/stdout. This is the only file where we're actually interacting with the
 | 
			
		||||
outside world, everything else is just pure code.
 | 
			
		||||
 | 
			
		||||
~~~haskell
 | 
			
		||||
instance MonadNetwork AppM where
 | 
			
		||||
  type Meta AppM = ()
 | 
			
		||||
  recvMsg = Request <$> liftIO T.getLine <*> pure ()
 | 
			
		||||
  putMsg Response{..} = liftIO . T.putStrLn $ content
 | 
			
		||||
~~~
 | 
			
		||||
 | 
			
		||||
Writing the extensions was the responsibility of *external contributors* and we
 | 
			
		||||
already saw how the actual extension point was defined above. Using these
 | 
			
		||||
extension points is really simple and here we see how the implementation is
 | 
			
		||||
just a simple `Request -> m (Maybe Response)`.
 | 
			
		||||
 | 
			
		||||
~~~haskell
 | 
			
		||||
extension :: Extension ()
 | 
			
		||||
extension = Extension{..}
 | 
			
		||||
  where
 | 
			
		||||
    name = "hello world"
 | 
			
		||||
    act Request{..} | "hello" `T.isPrefixOf` content = return $ Just $ Response "Hello to you" ()
 | 
			
		||||
                    | otherwise = return Nothing
 | 
			
		||||
~~~
 | 
			
		||||
		Reference in New Issue
	
	Block a user