Реальному коду зачастую нужно несколько эффектов сразу: состояние + ошибки + IO. Но монады, в отличие от функторов, не композируются автоматически: в общем случае не монада. Трансформер — версия монады со «слотом» для другой монады.
Определяются трансформеры как те же монады, но с параметром-монадой внутри:
newtype MaybeT m a = MaybeT (m (Maybe a))
newtype ExceptT e m a = ExceptT (m (Either e a))
newtype StateT s m a = StateT (s -> m (a, s))Существует такая монада Identity, определяется она так:
newtype Identity a = Identity { runIdentity :: a }
instance Monad Identity where
return = Identity -- обернуть значение
Identity x >>= f = f x -- и сразу развернутьОбычные монады — те же трансформеры над тождественной монадой:
type State s = StateT s Identity
MaybeT Identity a ≅ Maybe a -- Maybe не type-синоним, но изоморфнаЧерез слои стека действия базовой монады поднимает ; его законы требуют согласованности с и :
class MonadTrans t where
lift :: Monad m => m a -> t m a
lift . pure == pure
lift (m >>= f) == lift m >>= (lift . f)Порядок стека — это семантика: один и тот же набор эффектов, разный порядок — разное поведение при ошибке:
StateT s (Except e) a ≅ s -> Either e (a, s) -- ошибка съедает состояние
ExceptT e (State s) a ≅ s -> (Either e a, s) -- состояние переживает ошибкуСоберём сказанное на живом примере: цепочка IO-действий, каждое из которых может не дать результата (переменной окружения нет, файла нет). Без трансформера пришлось бы проверять после каждого шага; прячет эти проверки в do-нотацию — первый же обрывает всю цепочку.
import Control.Monad.Trans.Maybe (MaybeT(..), runMaybeT)
import Control.Monad.Trans.Class (lift)
import System.Environment (lookupEnv) -- :: String -> IO (Maybe String)
import System.Directory (doesFileExist)
readConfig :: FilePath -> IO (Maybe String) -- Nothing, если файла нет
readConfig path = do
exists <- doesFileExist path
if exists then Just <$> readFile path else pure Nothing
-- IO + возможный провал в одной do-нотации:
loadConfig :: MaybeT IO String
loadConfig = do
lift (putStrLn "reading config...") -- обычное IO поднимаем через lift
path <- MaybeT (lookupEnv "CONFIG") -- нет переменной -> вся цепочка Nothing
MaybeT (readConfig path) -- нет файла -> вся цепочка Nothing
main :: IO ()
main = do
result <- runMaybeT loadConfig -- runMaybeT :: MaybeT IO a -> IO (Maybe a)
case result of
Just cfg -> putStrLn ("ok: " ++ cfg)
Nothing -> putStrLn "config not found"