53

私はHaskellを初めて使用しますが、モナド変換子の使用方法を理解しています。それでも、関数呼び出しにパラメーターを渡すことに対して、彼らが主張する利点をつかむのはまだ困難です。

wikiモナド変換子の説明に基づいて、基本的に次のように定義された構成オブジェクトがあります

data Config = Config Foo Bar Baz

この署名で関数を書く代わりに、それを渡すために

client_func :: Config -> IO ()

ReaderTモナド変換子を使用して署名を次のように変更します

client_func :: ReaderT Config IO ()

Configをプルすることは、への単なる呼び出しaskです。

関数呼び出しがからclient_func cに変わりますrunReaderT client_func c

罰金。

しかし、なぜこれが私のアプリケーションを単純にするのですか?

1-多くの関数/モジュールをつなぎ合わせてアプリケーションを形成する場合、モナド変換子に関心があると思います。しかし、これが私の理解が止まるところです。誰かが光を当ててくれませんか?

2-モジュールが何らかの形式のAPIを公開して実装を非表示にし、(部分的に)独自の状態と環境を他のモジュールから非表示にする、Haskellで大規模なモジュラーアプリケーションを作成する方法に関するドキュメントが見つかりませんでした。ポインタはありますか?

(編集:Real World Haskellは、「..このアプローチ[モナド変換子] ...はより大きなプログラムに拡張できる」と述べていますが、その主張を示す明確な例はありません)

以下のクリス・テイラーの回答に続いて編集

クリスは、Config、StateなどをTransformerMonadにカプセル化することで2つの利点が得られる理由を完全に説明しています。

  1. これにより、上位レベルの関数が、呼び出す(サブ)関数に必要であるが、それ自体の使用には必要のないすべてのパラメーターを型アノテーションで維持する必要がなくなります(getUserInput関数を参照) 。
  2. Writerその結果、トランスフォーマーモナドのコンテンツの変更に対して、より高いレベルの関数の復元力が高まります(たとえば、より低いレベルの関数でのロギングを提供するために、それに追加したい場合)

これには、Transformerモナドの「内部」で実行されるようにすべての関数のシグネチャを変更するという犠牲が伴います。

したがって、質問1は完全にカバーされています。クリスありがとう。

質問2はこのSO投稿で回答されました

4

1 に答える 1

51

次の形式の構成情報を必要とするプログラムを作成しているとしましょう。

data Config = C { logFile :: FileName }

プログラムを作成する1つの方法は、関数間で構成を明示的に渡すことです。明示的に使用する関数に渡すだけでよいのですが、残念ながら、ある関数が構成を使用する別の関数を呼び出す必要があるかどうかわからないため、強制的にどこにでもあるパラメーター(実際、構成を使用する必要があるのは低レベルの関数である傾向があり、そのため、すべての高レベルの関数にもそれを渡す必要があります)。

そのようなプログラムを書いてみましょう。それから、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

高レベルの関数では、常にconfigを渡す必要があることに注意してください。

オプション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。引き続き正常に機能します。

注意:私はこの投稿をタイプチェックしていません。ましてや実行しようとしました。エラーがあるかもしれません!

于 2012-10-19T08:09:34.577 に答える