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

Модульный дизайн программы - Объединение Monad Transformers в Monad Agnostic функции

Я пытаюсь придумать модульный дизайн программы, и я, еще раз, любезно прошу вашу помощь.

Как продолжение следующих сообщений Monad Transformers vs pass Parameters и Масштабный дизайн в Haskell Я пытаюсь создать два независимых модуля, которые используют Monad Transformers, но выставляют Monad-agnostic функции, а затем объединяют Monad-agnostic функцию от каждого из этих модулей в новую Monad-агностическую функцию.

Мне не удалось запустить функцию объединения, например. как я могу вызвать mainProgram с помощью runReaderT в примере ниже?.

Дочерний вопрос: есть ли лучший способ достичь той же модульной цели дизайна?


В примере есть два модуля mock (но компиляции), один из которых выполняет регистрацию, и тот, который читает ввод пользователя и манипулирует им. Функция объединения считывает пользовательский ввод, регистрирует его и печатает.

{-# LANGUAGE FlexibleContexts #-}

module Stackoverflow2 where

import Control.Monad.Reader

----
---- From Log Module - Writes the passed message in the log
---- 

data LogConfig = LC { logFile :: FilePath }

doLog :: (MonadIO m, MonadReader LogConfig m) => String -> m ()
doLog _ = undefined


----
---- From UserProcessing Module - Reads the user Input and changes it to the configured case
----

data  MessageCase = LowerCase | UpperCase deriving (Show, Read)

getUserInput :: (MonadReader MessageCase m, MonadIO m) => m String
getUserInput = undefined

----
---- Main program that combines the two
----                  

mainProgram :: (MonadReader MessageCase m, MonadReader LogConfig m, MonadIO m) => m ()
mainProgram = do input <- getUserInput
                 doLog input
                 liftIO $ putStrLn $ "Entry logged: " ++ input
4b9b3361

Ответ 1

Ваша подпись mainProgram проблематична, потому что класс MonadReader содержит функциональную зависимость MonadReader r m | m -> r. Это по существу означает, что один конкретный тип не может иметь экземпляр MonadReader для нескольких разных типов. Поэтому, когда вы говорите, что тип m имеет оба экземпляра MonadReader MessageCase и MonadReader LogConfig, он идет против объявления зависимости.

Самое простое решение - изменить mainProgram на наличие не общего типа:

mainProgram :: ReaderT MessageCase (ReaderT LogConfig IO) ()
mainProgram = do input <- getUserInput
                 lift $ doLog input
                 liftIO $ putStrLn $ "Entry logged: " ++ input

Для этого также требуется явный lift для doLog.

Теперь вы можете запустить mainProgram, выполнив каждый ReaderT отдельно, например:

main :: IO ()
main = do
    let messageCase = undefined :: MessageCase
        logConfig   = undefined :: LogConfig
    runReaderT (runReaderT mainProgram messageCase) logConfig

Если вы хотите иметь общую функцию, которая использует два разных экземпляра MonadReader, вам нужно сделать это явным в сигнатуре, что один читатель является монадным трансформатором поверх другого читателя.

mainProgram :: (MonadTrans mt, MonadReader MessageCase (mt m), MonadReader LogConfig m, MonadIO (mt m), MonadIO m) => mt m ()
mainProgram = do input <- getUserInput
                 lift $ doLog input
                 liftIO $ putStrLn $ "Entry logged: " ++ input

Однако у этого есть неудачный эффект, что функция больше не является полностью общей, потому что порядок, в котором два считывателя появляются в стеке монады, заблокирован. Может быть, есть более чистый способ достичь этого, но я не смог понять этого из головы, не жертвуя (даже больше) родословной.

Ответ 2

Существует способ написать полностью модульную версию программы. То, как вам нужно подходить к проблеме, состоит в том, чтобы объединить конфигурацию вашего читателя в одну структуру данных, а затем определить классы типов, которые описывают частичный интерфейс, который нужны конкретным функциям для этой структуры данных. Например:

class LogConfiguration c where
  logFile :: c -> FilePath

doLog :: (MonadIO m, LogConfiguration c, MonadReader c m) => String -> m ()
doLog = do
  file <- asks logFile
  -- ...

class MessageCaseConfiguration c where
  isLowerCase :: c -> Bool

getUserInput :: (MonadIO m, MessageCaseConfiguration c, MonadReader c m) => m String
getUserInput = do
  lc <- asks isLowerCase
  -- ...

data LogConfig = LC { logConfigFile :: FilePath }
data MessageCase = LowerCase | UpperCase

data Configuration = Configuration { logging :: LogConfig, casing :: MessageCase }

instance LogConfiguration Configuration where
  logFile = logConfigFile . logging

instance MessageCaseConfiguration Configuration where
  isLowerCase c = case casing c of
    LowerCase -> True
    UpperCase -> False

mainProgram :: (MonadIO m, MessageCaseConfiguration c, LogConfiguration c, MonadReader c m) => m ()
mainProgram = do
  input <- getUserInput
  doLog input
  liftIO . putStrLn $ "Entry logged: " ++ input

Теперь вы можете вызвать mainProgram с помощью Configuration в монаде ReaderT, и он будет работать так, как вы ожидали.