Upload
others
View
3
Download
0
Embed Size (px)
Citation preview
co-log: Composable Contravariant Comonadic Logging Component by Dmitrii Kovanikov
15 May 2019
About me
Haskell Lecturer
Haskell Developer Co-founder, mentor,maintainer
2 / 38@chshersh
Decomposinglogging task
■ What to log: raw text, message data type, Map Key Value■ Where to log: stdout/stderr, file, database, external service■ How to format output: raw text, coloured text, JSON■ Context: IO, pure, custom monad
3 / 38
Structure of theco-log logging framework
■ co-log-core: fundamental reusable abstractions■ co-log: monadic tagless final implementation of logging■ co-log-polysemy: logging based on extensible effects■ co-log-benchmarks: performance measurements
4 / 38
Core data type:LogAction
newtype LogAction m msg = LogAction
{ unLogAction :: msg -> m ()
}
logStringStdout :: LogAction IO String
logStringStdout = LogAction putStrLn
5 / 38
LOGGER — VALUEThe main idea is to treat logging action as a value instead of a function that performs some side effects when called.
6 / 38
How to use loggerif it is a value?
1. Pass explicitly as an argument.2. Store inside monadic context so it can be extracted and used automatically.
7 / 38
1.ComposabilitySemigroup and Monoid typeclasses
8 / 38
<>
Semigrouptypeclass
class Semigroup m where
(<>) :: m -> m -> m
instance Applicative m => Semigroup (LogAction m msg) where
(<>) :: LogAction m msg -> LogAction m msg -> LogAction m msg
LogAction l1 <> LogAction l2 =
LogAction $ \msg -> l1 msg *> l2 msg
Semigroup: perform multiple actions over the same message9 / 38
Monoidtypeclass
class Semigroup m => Monoid m where
mempty :: m
instance Applicative m => Monoid (LogAction m msg) where
mempty :: LogAction m msg
mempty = LogAction $ \_ -> pure ()
Monoid: empty logging action that does nothing10 / 38
2.ContravarianceContravariant, Divisible and Decidable typeclasses
11 / 38
Contravarianttypeclass
class Contravariant f where
contramap :: (a -> b) -> f b -> f a
class Functor f where
fmap :: (a -> b) -> f a -> f b
fmap :: (a -> b) -> f a -> f b
contramap :: (a -> b) -> f b -> f a12 / 38
Contravariantinstance for LogAction
instance Contravariant (LogAction m) where
contramap :: (a -> b) -> LogAction m b -> LogAction m a
contramap f (LogAction action) = LogAction (action . f)
Contravariant: ability to change type of the consumed message
13 / 38
Contravariant use case:Formatting
data Message = Message
{ messageText :: String
, messageTags :: [Tag]
}
formatMessage :: Message -> String
logMessageStdout :: LogAction IO MessagelogMessageStdout = contramap formatMessage logStringStdout
14 / 38
MonadicContravariant
cmapM :: Monad m
=> (a -> m b) -> LogAction m b -> LogAction m acmapM f (LogAction action) = LogAction (action <=< f)
15 / 38
Compare:
contramap f (LogAction action) = LogAction (action . f)cmapM f (LogAction action) = LogAction (action <=< f)
MonadicContravariant use casedata Message = Message
{ messageText :: String
, messageTime :: UTCTime
}
withTime :: String -> IO Message
withTime txt = Message txt <$> getCurrentTime
logUtcStringStdout :: LogAction IO StringlogUtcStringStdout = cmapM withTime logMessageStdout
16 / 38
Contravariantfilter
cfilter
:: Applicative m
=> (msg -> Bool)
-> LogAction m msg
-> LogAction m msg
cfilter p (LogAction action) = LogAction $ \msg -> when (p msg) (action msg)
17 / 38
Contravariantfilter use case
data Severity = Debug | Info | Warning | Error ...
data Message = Message
{ messageSeverity :: Severity
, messageText :: String
}
logWarningMessageStdout :: LogAction IO Message
logWarningMessageStdout = cfilter
(\(Message sev _) -> sev >= Warning) logMessageStdout 18 / 38
Divisibletypeclass
class Contravariant f => Divisible f where
conquer :: f a
divide :: (a -> (b, c)) -> f b -> f c -> f a
Divisible: split a message into two parts and log each piece
19 / 38
Divisibleinstance
instance Applicative m => Divisible (LogAction m) where
conquer :: LogAction m a
conquer = mempty
divide :: (a -> (b, c))
-> LogAction m b
-> LogAction m c
-> LogAction m a
divide f (LogAction logB) (LogAction logC) = LogAction $ \(f -> (b, c)) -> logB b *> logC c
20 / 38
Decidabletypeclass
class Divisible f => Decidable f where
lose :: (a -> Void) -> f a choose :: (a -> Either b c) -> f b -> f c -> f a
Decidable: decide what part of the message to log
21 / 38
22 / 38
Decidableinstanceinstance Applicative m => Decidable (LogAction m) where
lose :: (a -> Void) -> LogAction m a
lose f = LogAction (absurd . f)
choose :: (a -> Either b c)
-> LogAction m b
-> LogAction m c
-> LogAction m a
choose f (LogAction logB) (LogAction logC) = LogAction (either logB logC . f)
23 / 38
Decidableuse case
cfilter
:: Applicative m
=> (a -> Bool)
-> LogAction m a
-> LogAction m a
cfilter p action = choose
(\msg -> if p msg then Left () else Right msg)
mempty action
24 / 38
3.ComonadsComonad typeclass
25 / 38
Comonadtypeclass
class Functor w => Comonad w where
extract :: w a -> a
extend :: (w a -> b) -> w a -> w b duplicate :: w a -> w (w a)
26 / 38
Comonad instancefor the function arrow
instance Monoid m => Comonad ((->) m) where
extract :: (m -> a) -> a
extract f = f mempty
duplicate :: (m -> a) -> (m -> m -> a) duplicate f = \m1 m2 -> f (m1 <> m2)
27 / 38
Implementing comonadic ideasfor LogAction: extract
extract :: Monoid msg => LogAction m msg -> m ()extract (LogAction action) = action mempty
28 / 38
Implementing comonadic ideasfor LogAction: duplicate
duplicate
:: Semigroup msg
=> LogAction m msg
-> LogAction m (msg, msg)
duplicate (LogAction action) = LogAction $ \(m1, m2) -> action (m1 <> m2)
29 / 38
Implementing comonadic ideasfor LogAction: multiplicate
multiplicate
:: (Foldable f, Semigroup msg)
=> LogAction m msg
-> LogAction m (f msg)
multiplicate (LogAction action) = LogAction $ \msgs -> action (fold msgs)
30 / 38
Abstractionsrecap
■ Semigroup: multiple actions over a single message (concurrency)■ Monoid: empty logger that does nothing (disabling logging)■ Contravariant: change the type of the message (formatting)■ Divisible: log batch of messages independently (modularity)■ Decidable: decide what to log (message filtering)■ Comonad: combine multiple messages into one (optimization)
31 / 38
4.co-log in production applicationsHow to use co-log in your Haskell application?
32 / 38
How we use co-logStep 1: Application environment
Create an environment that stores LogAction
33 / 38
data Env m = Env
{ envPort :: Port
, envLogAction :: LogAction m Message }
How we use co-logStep 2: Implement HasLog instance
Lens-like typeclass for accessing and modifying LogAction
34 / 38
instance HasLog (Env m) Message m where
getLogAction :: Env m -> LogAction m Message
getLogAction = envLogAction
setLogAction :: LogAction m msg -> Env m -> Env m
setLogAction newAction env =
env { envLogAction = newAction }
How we use co-logStep 3: Create monad for your application
newtype App a = App
{ unApp :: ReaderT (Env App) IO a
} deriving ( Functor, Applicative, Monad , MonadIO, MonadReader (Env App))
35 / 38
How we use co-logStep 4: Write function that uses logger
smsSendTestHandler
:: (MonadSms m, WithLog env Message m)
=> Phone
-> m ()
smsSendTestHandler phone = do
log D $ "Sending test sms to: " <> show phone sendSms phone "Test message"
36 / 38
Output example
37 / 38
Ecosystem
■ co-log: 4 core Haskell packages■ co-log-sys: Syslog implementation of co-log■ scala-colog: Scala implementation of the co-log ideas■ three-layer: Example of using co-log with the Three
Layer Cake architecture■ Blog post: co-log: Composable Contravariant
Combinatorial Comonadic Configurable Convenient Logging
38 / 38
Thanks!!Questions?
Credits
Icons made by Freepik from www.flaticon.com is licensed by CC 3.0 BY.