15

私はモジュール式のプログラム設計を考え出そうとしており、もう一度、あなたの助けをお願いします。

これらの次の投稿のフォローアップとして、 Monad Transformers と Haskell でのパラメーターと大規模な設計の受け渡し、Monad Transformers を使用するが Monad に依存しない関数を公開する 2 つの独立したモジュールを構築してから、それぞれの Monad に依存しない関数を結合しようとしています。これらのモジュールを新しい Monad に依存しない関数に変換します。

mainProgram結合関数を実行できませんでした。たとえば、以下の例でusingを呼び出すにはどうすればよいrunReaderTですか?

副次的な質問は、同じモジュール設計の目標を達成するためのより良い方法はありますか?


この例には 2 つのモック モジュール (ただしコンパイル) があり、1 つはログ記録を実行し、もう 1 つはユーザー入力を読み取って操作します。結合関数は、ユーザー入力を読み取り、ログに記録して出力します。

{-# 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
4

2 に答える 2

22

プログラムの完全にモジュール化されたバージョンを作成する方法があります。この問題に対処するために必要な方法は、リーダー構成を 1 つのデータ構造にまとめてから、特定の関数がそのデータ構造に対して必要とする部分的なインターフェイスを記述する型クラスを定義することです。例えば:

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

これで、モナド内で amainProgramを呼び出すことができ、期待どおりに動作します。ConfigurationReaderT

于 2012-10-22T11:06:10.280 に答える
11

型クラスに機能的な依存関係が含まれているため、署名mainProgramに問題があります。これは基本的に、1 つの具象型が複数の異なる型のインスタンスを持つことができないことを意味します。したがって、型に両方のインスタンスがあり、依存関係の宣言に反すると言うと。MonadReaderMonadReader r m | m -> rMonadReadermMonadReader MessageCaseMonadReader LogConfig

mainProgram最も簡単な解決策は、非ジェネリック型に変更することです。

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

これには、明示的な for も必要liftですdoLog

mainProgramこれで、次のように、それぞれをReaderT個別に実行して実行できます。

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

2 つの異なるインスタンスを使用するジェネリック関数がMonadReader必要な場合は、1 つのリーダーが他のリーダーのモナド変換子であることを署名で明示する必要があります。

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 つのリーダーの順序がロックされるからです。これを達成するためのよりクリーンな方法があるかもしれませんが、(さらに) 汎用性を犠牲にすることなく、頭のてっぺんから 1 つを理解することはできませんでした。

于 2012-10-22T09:38:48.517 に答える