Подтвердить что ты не робот

Monad Transformers vs Передача параметров в функции

Я новичок в Haskell, но понимаю, как Monad Transformers можно использовать. Тем не менее, я все еще испытываю трудности с захватом заявленного преимущества перед передачей параметров для вызова функций.

Основываясь на wiki Разъяснение трансформаторов Monad, у нас в основном есть объект Config, определенный как

data Config = Config Foo Bar Baz

и передать его, вместо того, чтобы писать функции с этой сигнатурой

client_func :: Config -> IO ()

мы используем трансформатор ReaderT Monad Transformer и меняем подпись на

client_func :: ReaderT Config IO ()

вытаскивание Config - это просто вызов ask.

Вызов функции изменяется с client_func c на runReaderT client_func c

Fine.

Но почему это упрощает мое приложение?

1- Я подозреваю, что Monad Transformers заинтересованы, когда вы сшиваете много функций/модулей вместе, чтобы сформировать приложение. Но это то, где мое понимание прекращается. Может ли кто-нибудь пролить свет?

2- Я не смог найти документацию о том, как вы пишете большое приложение modular в Haskell, где модули выставляют какую-то форму API и скрывают их реализации, а также (частично) скрывают свои собственные Состояния и среды из других модулей. Любые указатели, пожалуйста?

(Edit: Real World Haskell заявляет, что ".. этот подход [Monad Transformers]... масштабируется для больших программ", но нет четкого примера, демонстрирующего это утверждение)

РЕДАКТИРОВАТЬ Следующий Крис Тейлор Отвечать ниже

Крис прекрасно объясняет, почему инкапсуляция Config, State и т.д. в Transformer Monad обеспечивает два преимущества:

  • Это препятствует тому, чтобы функция более высокого уровня поддерживала в своей сигнатуре типа все параметры, требуемые функциями (под), которые она вызывает, но не обязательными для ее собственного использования (см. функцию getUserInput)
  • и, как следствие, делает функции более высокого уровня более устойчивыми к изменению содержимого трансформаторной монады (скажем, вы хотите добавить к нему Writer для обеспечения входа в систему с более низким уровнем)

Это связано с изменением сигнатуры всех функций, поэтому они запускают "в" трансформаторную монаду.

Итак, вопрос 1 полностью покрыт. Спасибо Крису.

Вопрос 2 теперь отвечает на этот SO-сообщение

4b9b3361

Ответ 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. Я не проверял этот пост, не говоря уже пробовал его запустить. Могут быть ошибки!