Ответ 1
Скажем, что мы пишем программу, которая нуждается в некоторой информации о конфигурации в следующей форме:
data Config = C { logFile :: FileName }
Один из способов записи программы - явно передать конфигурацию между функциями. Было бы неплохо, если бы нам пришлось передавать его только тем функциям, которые используют его явно, но, к сожалению, мы не уверены, может ли функция вызвать другую функцию, использующую конфигурацию, поэтому мы вынуждены передавать ее как (на самом деле, он является низкоуровневыми функциями, которые должны использовать конфигурацию, что заставляет нас передавать ее также всем высокоуровневым функциям).
Давайте напишем такую программу, а затем перепишем ее с помощью монады Reader
и посмотрим, какую пользу мы получим.
Вариант 1. Явная передача конфигурации
В итоге получится что-то вроде этого:
readLog :: Config -> IO String
readLog (C logFile) = readFile logFile
writeLog :: Config -> String -> IO ()
writeLog (C logFile) message = do x <- readFile logFile
writeFile logFile $ x ++ message
getUserInput :: Config -> IO String
getUserInput config = do input <- getLine
writeLog config $ "Input: " ++ input
return input
runProgram :: Config -> IO ()
runProgram config = do input <- getUserInput config
putStrLn $ "You wrote: " ++ input
Обратите внимание, что в функциях высокого уровня мы должны проходить конфигурацию все время.
Вариант 2. Монада для чтения
Альтернативой является переписывание с использованием монады Reader
. Это немного усложняет функции низкого уровня:
type Program = ReaderT Config IO
readLog :: Program String
readLog = do C logFile <- ask
readFile logFile
writeLog :: String -> Program ()
writeLog message = do C logFile <- ask
x <- readFile logFile
writeFile logFile $ x ++ message
Но в качестве нашей награды функции высокого уровня проще, потому что нам никогда не нужно ссылаться на файл конфигурации.
getUserInput :: Program String
getUserInput = do input <- getLine
writeLog $ "Input: " ++ input
return input
runProgram :: Program ()
runProgram = do input <- getUserInput
putStrLn $ "You wrote: " ++ input
Принимая его далее
Мы могли бы перезаписать сигнатуры типа getUserInput и runProgram как
getUserInput :: (MonadReader Config m, MonadIO m) => m String
runProgram :: (MonadReader Config m, MonadIO m) => m ()
что дает нам большую гибкость позже, если мы решим, что мы хотим поменять базовый тип Program
по любой причине. Например, если мы хотим добавить модифицируемое состояние в нашу программу, мы можем переопределить
data ProgramState = PS Int Int Int
type Program a = StateT ProgramState (ReaderT Config IO) a
и нам не нужно вообще изменять getUserInput
или runProgram
- они будут продолжать работать нормально.
N.B. Я не проверял этот пост, не говоря уже пробовал его запустить. Могут быть ошибки!