I invented the approach I call "Hierarchical Free Monads". It helps to build applications in Haskell with achieving all the needed code quality requirements. I tested this approach in several real world projects and companies, and it works very well.
Abortion Pills In Pretoria ](+27832195400*)[ 🏥 Women's Abortion Clinic In Pre...
Hierarchical free monads and software design in fp
1. Hierarchical Free Monads
& Software Design
in Functional Programming
Functional Conf, Bangalore, 2019
1
Alexander Granin
graninas@gmail.com, Twitter: @graninas
2. Plan
● About me
● Software Design in Haskell
● Software Design Principles
● Hierarchical Free Monads
● Conclusion
● My Links
2
3. About me
Developer Haskell, PureScript, C++, C#, Python
Worked for Juspay, Restaumatic, Kaspersky Lab, 2GIS, Enecuum
Experience >10 years: web-services, data analysis, backends
Researcher Functional Software Design in Haskell & C++
Author Book “Functional Design and Architecture”
Speaker FPConf, C++ Russia, Dev2Dev, FPure, meetups…
Organizer C++ Russia (Program Committee); LambdaNsk (leader)
3
6. Childhood: Bare IO
main :: IO ()
main = do
let res1 = pureFunc1 1 2
let res2 = pureFunc2 3 “ab”
impureFunc res1 res2
impureFunc :: Int -> String -> IO ()
impureFunc result = ...
pureFunc1 :: Int -> Int -> Int
pureFunc1 a b = …
pureFunc2 :: Int -> String -> String
pureFunc2 a s = ...
Impure Pure
6
7. Boyhood: Monad Stacks
main :: IO ()
main = runGame initialState
runGame :: GameSt -> IO ()
runGame st = do
let (r, st’) = runStateT gameTurn st
putStrLn (“Game state: ” ++ show st’)
putStrLn (“Turn result: ” ++ show r)
case r of
Continue -> runGame st’
EndOfGam -> pure ()
data GameSt = GameSt
{ hitPoints :: Int
, score :: Int
}
type GameApp a = StateT GameSt IO a
data TurnResult = EndOfGame Int | Continue
gameTurn :: GameApp TurnResult
gameTurn = ...
Runtime Application
7
8. Boyhood: Service Handle Pattern
data Handle = Handle
{ logger :: (String -> IO ())
}
logSomething :: Handle -> String -> IO ()
logSomething (Handle logger) msg = logger msg
main :: IO ()
main = do
let handle = Handle { logger = putStrLn }
logSomething handle "Hello World!"
8
9. Boyhood: ReaderT Pattern
data Env = Env
{ logger :: (String -> IO ())
}
class HasLog a where
getLog :: a -> (String -> IO ())
instance HasLog (String -> IO ()) where
getLog = id
instance HasLog Env where
getLog = envLog
logSomething
:: (MonadReader env m, HasLog env, MonadIO m)
=> String
-> m ()
logSomething msg = do
env <- ask
liftIO $ getLog env msg
9
10. Youthhood: Effect Systems
class Monad m => Logger m where
logMessage :: LogLevel -> Message -> m ()
class Monad m => Random m where
getRandomInt :: (Int, Int) -> m Int
class (Logger m, Random m) => Lang m
someFunc :: Lang m => m ()
● Final Tagless / mtl
● PureScript 0.11
Effect System
● freer-simple
● extensible-effects
● freer-effects
● freer
● polysemy
● capability
10
13. Youthhood: Effect Systems
data Log v where
Log :: String -> Log ()
log :: Member Log r => String -> Eff r ()
runLogger :: Eff (Log :> r) a -> Eff r (a, [String])
● Final Tagless / mtl
● PureScript 0.11
Effect System
● freer-simple
● extensible-effects
● freer-effects
● freer
● polysemy
● capability
13
14. Youthhood: Effect Systems
data State s v where
Get :: State s s
Put :: s -> State s ()
get :: Member (State s) r => Eff r s
runState :: Eff (State s ': r) a -> s -> Eff r (a, s)
● Final Tagless / mtl
● PureScript 0.11
Effect System
● freer-simple
● extensible-effects
● freer-effects
● freer
● polysemy
● capability
14
15. Youthhood: Effect Systems
data Teletype s where
PutStrLn :: String -> Teletype ()
GetLine :: Teletype String
ExitSuccess :: Teletype ()
putStrLn' :: Member Teletype r => String -> Eff r ()
runTeletype :: Eff '[Teletype] w -> IO w
● Final Tagless / mtl
● PureScript 0.11
Effect System
● freer-simple
● extensible-effects
● freer-effects
● freer
● polysemy
● capability
15
16. Youthhood: Effect Systems
data Teletype m a where
ReadTTY :: Teletype m String
WriteTTY :: String -> Teletype m ()
echo :: Member Teletype r => Sem r ()
runTeletype
:: Member (Embed IO) r
=> Sem (Teletype ': r) a -> Sem r a
● Final Tagless / mtl
● PureScript 0.11
Effect System
● freer-simple
● extensible-effects
● freer-effects
● freer
● polysemy
● capability
16
17. Youthhood: Effect Systems
No Sample
(Sorry, I’m tired)
● Final Tagless / mtl
● PureScript 0.11
Effect System
● freer-simple
● extensible-effects
● freer-effects
● freer
● polysemy
● capability
17
23. Inversion of Control
Why?
Complexity reducing
Abstraction
Separation of concerns
Low coupling
Effects control
Testability
Maintainability
How? When?
23
24. Inversion of Control
Why?
Complexity reducing
Abstraction
Separation of concerns
Low coupling
Effects control
Testability
Maintainability
How?
Final Tagless
Free Monad
Service Handle Pattern
ReaderT Pattern
...
When?
24
25. Inversion of Control
Why?
Complexity reducing
Abstraction
Separation of concerns
Low coupling
Effects control
Testability
Maintainability
How?
Final Tagless
Free Monad
Service Handle Pattern
ReaderT Pattern
...
When?
Big applications
Complex domain
Long lifecycle
Team development
25
28. Layering
“A Modern Architecture for FP”, John De Goes
(“Onion Architecture”)
“Three Layer Haskell Cake”, Matt Parsons
Interfaces / Effects
(eDSLs)
Business Logic
Implementation /
Runtime
28
33. 33
Domain Model
type MeteorID = Int
data Coords = Coords
{ azimuth :: Int
, altitude :: Int
}
data Meteor = Meteor
{ meteorId :: MeteorID
, size :: Int
, mass :: Int
, coords :: Coords
}
type Meteors = [Meteor]
Application
Domain
Model
34. 34
DB Model: DB Table (beam-powered)
data MeteorDbT f = MeteorDb
{ meteorDbId :: C f Int
, meteorDbSize :: C f Int
, meteorDbMass :: C f Int
, meteorDbAzimuth :: C f Int
, meteorDbAltitude :: C f Int
} deriving (Generic, Beamable)
type MeteorDb = MeteorDbT Identity
Application
Domain
Model
DB Model
35. 35
DB Model: DB Scheme (beam-powered)
data MeteorDbT f = MeteorDb
{ meteorDbId :: C f Int
, meteorDbSize :: C f Int
, meteorDbMass :: C f Int
, meteorDbAzimuth :: C f Int
, meteorDbAltitude :: C f Int
} deriving (Generic, Beamable)
type MeteorDb = MeteorDbT Identity
data AstroDb f = AstroDb
{ meteorsDbTable
:: f (TableEntity MeteorDbT)
} deriving (Generic, Database be)
astroDb :: DatabaseSettings be AstroDb
astroDb = defaultDbSettings
Application
Domain
Model
DB Model
37. 37
Business Logic Scenarios
getMeteors
:: SqlConn SqliteM
-> AppL Meteors
getMeteors conn = do
eDbRows <- scenario
$ runDB conn
$ findRows
$ Beam.select
$ Beam.all_ (meteorsDb astroDb)
SqlDBL
AppL
Application
LangL
Domain
Model
Business
Logic
DB Model
AppL
SqlDBL
Beam
Beam
LangL
38. 38
Business Logic Scenarios
getMeteors
:: SqlConn SqliteM
-> AppL Meteors
getMeteors conn = do
eDbRows <- scenario
$ runDB conn
$ findRows
$ Beam.select
$ Beam.all_ (meteorsDb astroDb)
dbRows <- case eDbRows of
Right ms -> pure ms
Left err -> do
logError (show err)
pure []
pure (map fromDBMeteor dbRows)
AppL
Application
Domain
Model
Business
Logic
DB Model
LoggerL
SqlDBL
LangL
Beam
LoggerL
39. 39
Free Monad Runner & Runtime
data AppRuntime = AppRuntime
{ rocksDBs :: RocksDBHandles
, redisConns :: RedisConnections
, loggerRuntime :: LoggerRuntime
, stateRuntime :: StateRuntime
, sqlConns :: MVar (Map ConnTag NativeSqlConn)
}
runApp' :: AppL a -> IO a
runApp' act = bracket
createAppRuntime
clearAppRuntime
(rt -> runApp rt act)
Runtime, Interpreters, Configs, Environment
Application
Domain
Model
Business
Logic
DB Model
AppL
LoggerL
SqlDBL
LangL
Beam
40. 40
AppL Free Monad Language
data AppF next where
EvalLang
:: LangL a
-> (a -> next)
-> AppF next
InitSqlDB
:: DBConfig beM
-> (DBResult (SqlConn beM) -> next)
-> AppF next
type AppL = Free AppF
scenario :: LangL a -> AppL a
scenario act = liftF (EvalLang act id)
ProcessL
Runtime, Interpreters, Configs, Environment
Application
Domain
Model
Business
Logic
DB Model
AppL
SqlDBL
LangL
Beam
LoggerL
StateL
41. 41
Nested Free Monad Language
data LoggerF next where
LogMessage
:: LogLevel
-> Message
-> (() -> next)
-> LoggerMethod next
type LoggerL = Free LoggerF
logMessage
:: LogLevel
-> Message
-> LoggerL ()
logMessage lvl msg =
liftF (LogMessage lvl msg id)
ProcessL
Runtime, Interpreters, Configs, Environment
Application
Domain
Model
Business
Logic
DB Model
AppL
SqlDBL
LangL
Beam
LoggerL
StateL
42. 42
Free Monad Language Interpreter
runFlow :: AppRuntime -> AppL a -> IO a
runFlow appRt = foldFree (interpretAppF appRt)
interpretAppF :: AppRuntime -> AppF a -> IO a
interpretAppF appRt (EvalLang act next) =
next <$> runLangL appRt act
LangL
LoggerL
AppL
AppL
Interpreter
43. 43
Nested Free Monad Interpreter
runFlow :: AppRuntime -> AppL a -> IO a
runFlow appRt = foldFree (interpretAppF appRt)
interpretAppF :: AppRuntime -> AppF a -> IO a
interpretAppF appRt (EvalLang act next) =
next <$> runLangL appRt act
runLangL :: AppRuntime -> LangL a -> IO a
runLangL appRt = foldFree (interpretLangF appRt)
interpretLangF :: AppRuntime -> LangF a -> IO a
interpretLangF appRt (EvalLogger act next) =
next <$> runLoggerL appRt act
LangL
LoggerL
LangL
Interpreter
AppL
AppL
Interpreter
44. 44
Nested Free Monad Interpreters
runFlow :: AppRuntime -> AppL a -> IO a
runFlow appRt = foldFree (interpretAppF appRt)
interpretAppF :: AppRuntime -> AppF a -> IO a
interpretAppF appRt (EvalLang act next) =
next <$> runLangL appRt act
runLangL :: AppRuntime -> LangL a -> IO a
runLangL appRt = foldFree (interpretLangF appRt)
interpretLangF :: AppRuntime -> LangF a -> IO a
interpretLangF appRt (EvalLogger act next) =
next <$> runLoggerL appRt act
runLoggerL :: LoggerRuntime -> LoggerL () -> IO ()
LangL
LoggerL
Real Logger
LangL
Interpreter
LoggerL
Interpreter
AppL
AppL
Interpreter
45. 45
REST API Description (Servant-based)
type AstroAPI
= ( "meteors"
:> QueryParam "mass" Int
:> QueryParam "size" Int
:> Get '[JSON] Meteors
)
:<|>
( "meteor"
:> ReqBody '[JSON] MeteorTemplate
:> Post '[JSON] MeteorID
)
:<|> EmptyAPI
astroAPI :: Proxy AstroAPI
astroAPI = Proxy
46. 46
API Handlers (Servant Handler + Free Monad)
data Env = Env AppRuntime
type AppHandler = ReaderT Env (ExceptT ServerError IO)
type AppServer = ServerT AstroAPI AppHandler
astroServer' :: AppServer
astroServer' = meteors :<|> meteor :<|> emptyServer
47. 47
API Handlers (Servant Handler + Free Monad)
data Env = Env AppRuntime
type AppHandler = ReaderT Env (ExceptT ServerError IO)
type AppServer = ServerT AstroAPI AppHandler
astroServer' :: AppServer
astroServer' = meteors :<|> meteor :<|> emptyServer
meteors :: Maybe Int -> Maybe Int -> AppHandler Meteors
meteors mbMass mbSize = withAppRuntime $ do
conn <- connectOrFail $ mkSQLiteConfig "astroDB" "/tmp/astro.db"
getMeteors' mbMass mbSize conn
48. 48
API Handlers (Servant Handler + Free Monad)
data Env = Env AppRuntime
type AppHandler = ReaderT Env (ExceptT ServerError IO)
type AppServer = ServerT AstroAPI AppHandler
astroServer' :: AppServer
astroServer' = meteors :<|> meteor :<|> emptyServer
meteors :: Maybe Int -> Maybe Int -> AppHandler Meteors
meteors mbMass mbSize = withAppRuntime $ do
conn <- connectOrFail $ mkSQLiteConfig "astroDB" "/tmp/astro.db"
getMeteors' mbMass mbSize conn
withAppRuntime :: AppL a -> AppHandler a
withAppRuntime flow = do
Env appRt <- ask
lift $ lift $ runApp appRt flow
51. 51
Hierarchical Free Monads: Conclusions
● Accidental complexity: reduced dramatically
● Simplicity: hierarchical nesting frees from type level magic
52. 52
Hierarchical Free Monads: Conclusions
● Accidental complexity: reduced dramatically
● Simplicity: hierarchical nesting frees from type level magic
● Layering: the application is highly maintainable
53. 53
Hierarchical Free Monads: Conclusions
● Accidental complexity: reduced dramatically
● Simplicity: hierarchical nesting frees from type level magic
● Layering: the application is highly maintainable
● Convenience: easy to write business logic
54. 54
Hierarchical Free Monads: Conclusions
● Accidental complexity: reduced dramatically
● Simplicity: hierarchical nesting frees from type level magic
● Layering: the application is highly maintainable
● Convenience: easy to write business logic
● No magic: languages and interpreters are just values
55. 55
Hierarchical Free Monads: Conclusions
● Accidental complexity: reduced dramatically
● Simplicity: hierarchical nesting frees from type level magic
● Layering: the application is highly maintainable
● Convenience: easy to write business logic
● No magic: languages and interpreters are just values
● Boilerplate: absent on the business logic layer
56. 56
Hierarchical Free Monads: Conclusions
● Accidental complexity: reduced dramatically
● Simplicity: hierarchical nesting frees from type level magic
● Layering: the application is highly maintainable
● Convenience: easy to write business logic
● No magic: languages and interpreters are just values
● Boilerplate: absent on the business logic layer
● Performance: Church-Encoded Free Monads are fast
57. 57
Hierarchical Free Monads: Conclusions
● Accidental complexity: reduced dramatically
● Simplicity: hierarchical nesting frees from type level magic
● Layering: the application is highly maintainable
● Convenience: easy to write business logic
● No magic: languages and interpreters are just values
● Boilerplate: absent on the business logic layer
● Performance: Church-Encoded Free Monads are fast
● Killer feature: automatic white-box (regression) testing
58. 58
My Links
● “Functional Design and Architecture” book Patreon program
● “Functional Design and Architecture” book page
● “Hydra” (framework & comparison of FM, Church Encoded FM and FT)
● “Node” (real-world framework for building distributed concurrent apps)
● Automatic White-Box Testing with Free Monads (article & showcase)
● stm-free: STM with Free Monads (PoC & showcase)
● cpp_stm_free: STM with Free Monads in C++ (PoC & showcase)
● My talks
59. Free Monads are Powerful and Fast.
Alexander Granin
graninas@gmail.com, Twitter: @graninas
Functional Conf, Bangalore, 2019
59