$ whoami
getHost :: IO String
getHost = getEnv "HOSTNAME"
main :: IO ()
main = do
host <- getHost
putStrLn host
// Using cats IO
def putStrLn(s: String): IO[Unit] =
IO.apply(println(s))
def getHost(): IO[String] = {
IO.apply(System.getEnv("HOSTNAME"))
}
even though it’s common to have runtime reflection, DI
containers and so on, DI at its core, is simple
cat :: CanIHaz Cheeseburger Nap
cat = CanIHaz (\cheez ->
-- whatever cats do
)
newtype CanIHaz env a =
CanIHaz (env -> a)
myOperation :: CanIHaz AppConfig MyValue
myOperation = do
value1 <- myOperation1
value2 <- myOperation2 value1
pure (value1 + value2)
( $ ) :: (a -> b) -> a -> b
(<$>) :: (a -> b) -> m a -> m b
(<*>) :: m (a -> b) -> m a -> m b
(=<<) :: (a -> m b) -> m a -> m b
pure :: a -> m a
( $ ) :: (a -> b) -> ( a -> b)
(<$>) :: (a -> b) -> (m a -> m b)
(<*>) :: m (a -> b) -> (m a -> m b)
(=<<) :: (a -> m b) -> (m a -> m b)
pure :: a -> m a
Combining steps separately is nice, but usually I don’t have
only one effect
async + DI + http errors
type Compose m n a = m (n a)
(>>=) :: (Monad m, Monad n)
=> Compose m n a
-> (a -> Compose m n b)
-> Compose m n b
the key point in all the examples above is that we use specific
properties of the data types to implement bind.
things exposed by Monad are not sufficient.
newtype MaybeT m a =
MaybeT { runMaybeT :: m (Maybe a) }
instance (Monad m) =>
Monad (MaybeT m) where
(>>=) = -- fun exercise
getSocket :: IO (Maybe String)
getSocket = runMaybeT $ do
host <- MaybeT (lookupEnv "HOST")
port <- MaybeT (lookupEnv "PORT")
pure (host <> ":" <> port)
case class OptionT[F[_],A](
value: F[Option[A]]
) {
def flatMap[B](f: A => OptionT[F,B])
: OptionT[F,B] = {
// fun exercise
}
}
def lookupEnv(s: String):
IO[Maybe[String] =
// todo
def getSocket(): IO[Maybe[String]] = (
for {
host <- OptionT(lookupEnv("HOST"))
port <- OptionT(lookupEnv("PORT"))
} yield s"${host}:${port}"
).value
findUserHandler :: UserId
-> HandlerWithConf User
findUserHander userId = do
results <- runDB (findUser userId)
case results of
Just u -> pure u
Nothing -> throwError err404 -- tbd
findUserHander(userId: UserId)
: HandlerWithConf[A]
= for {
results <- runT(findUser(userId))
response <- results match {
case Some(u) => pure(u) //tdb
case None => throwError(err404) //tbd
}
} yield response
HandlerWithConf
-- for ReaderT
ask :: MonadReader e m => m e
-- for ExceptT
throwError :: MonadError e m => e -> m a
-- for IO
liftIO :: MonadIO m => IO a -> m a
runDB :: ( Monad m
, MonadReader Config m
, MonadIO m)
=> Transaction a
-> m a
runDB t = do
pool <- asks dbPool
liftIO (runDbTransaction t pool)
findUser :: MonadEveryThing m
=> UserId
-> m User
findUser uid = do
results <- runTransaction $ findUser uid
case results of
Just u -> pure u
Nothing -> throwError err404
* conditions may apply
m (Either e a)
is fineMonadError
is less powerful than having an explicit either
(by design).
Put in the stack (or in constraints) what really needs to be
common across your application. Trying to fit everything
in the stack will waste your time and over constrain
your application