私は、私がいることを書いた
最初のIOモナドとの接触に入って来た、そして私は約たぶんモナドとListモナドを学びました。実際には、GHC-pkgコマンドを使用して表示できMTLパッケージにあります(などライターモナド、リーダーモナド、国家モナド、など)多くのモナドが存在します:
$ ghc-pkg list | grep mtl
mtl-2.2.1
PSHaskellプラットフォームにはデフォルトでmtlパッケージが含まれており、手動でインストールする必要はありません
1.ライターモナド
追跡実行プロセス
再帰的アルゴリズムを理解する場合、中間プロセスを記録するという強い要求があります。これは当時のやり方でした:
import Debug.Trace
d f = trace ("{" ++ show f ++ "}") f
トレースを介してログを追加します::文字列-> a-> a:
When called, trace outputs the string in its first argument, before returning the second argument as its result.
文字列と値を受け入れ、文字列を出力して、入力値をそのまま返します。次に例を示します。
> x `add` y = trace (show x ++ " + " ++ show y) (x + y)
> add 3 $ add 1 2
1 + 2
3 + 3
6
実行プロセスは正常にトレースされますが、ソースコードを変更する必要があります。各関数をログ付きのバージョンに変更するのは面倒なので、ツール関数dを使用して実行します(知りたいことは何でも)。
> d (1 + 2) + 3
{3}
6
例として、古典的なHaskellクイックソートを取り上げます。
quickSort [] = []
quickSort (x:xs) = quickSort ltX ++ [x] ++ quickSort gtX
where ltX = [a | a <- xs, a <= x]
gtX = [a | a <- xs, a > x]
ログを追加します。左側の処理プロセスを参照してください(d ltX):
quickSortWithLog [] = []
quickSortWithLog (x:xs) = quickSortWithLog (d ltX) ++ [x] ++ quickSortWithLog gtX
where ltX = [a | a <- xs, a <= x]
gtX = [a | a <- xs, a > x]
やってみよう:
> quickSortWithLog [9, 0, 8, 10, -5, 2, 13, 7]
{[0,8,-5,2,7]}
{[-5]}
{[]}
[-5,0{[2,7]}
{[]}
,2{[]}
,7,8,9{[]}
,10{[]}
,13]
ログによると、最初のトリップの左側は[0,8、-5,2,7](ピボットは9)、次は[-5](ピボットは0)、左側は[](ピボットは-5)、(0)の反対側の最初のトリップは左側の[2,7]で、続行すると左側が[]になります。元の配列の左側が処理され、右側も同様なので、繰り返しません
問題をかろうじて解決することはできますが、いくつかの欠点があります。
ログ出力は結果に混在しており、ログはあまり直感的に見えません
ログは元の結果出力に影響し、分離されません
印刷することしかできず、さらに処理するために収集する方法はなく、十分な柔軟性がありません
それで、実行プロセスを追跡したい場合、よりエレガントな方法はありますか?
Writer
は、計算中に操作ログを自動的に維持できますか?
追加のログ情報をコンテキストと見なすと、Monadと関係があるようです。たとえば、値が計算に含まれている間、ログを自動的に収集(このコンテキストを維持)できます。
これがライターの起源です。
Writerは、ログと同じように、付加価値のあるコンテキストを追加します
Writerを使用すると、計算中にすべてのログレコードを収集し、ログを統合して結果に添付することができます
ライターは次のようになります。
type Writer w = WriterT w Identity
newtype WriterT w m a = WriterT { runWriterT :: m (a, w) }
型宣言の観点から、Writerはタプル((a、w))のラッパーであり、mはIDとして指定されます。
newtype Identity a = Identity { runIdentity :: a }
instance Applicative Identity where
pure = Identity
instance Monad Identity where
m >>= k = k (runIdentity m)
役に立たないようです。よく見てください。Identityというパッケージタイプが宣言され、Monadも実装されています。returnの動作は指定された値をラップすることであり、>> =の動作は、左側にラップされた値に右側を適用することです。関数。私はまだ何の用途も見つけていません...実際、これはモナドの世界ではid :: a-> aと同等であり、値をモナドにラップして計算に参加できます。さらに、何もしません。アプリケーションシナリオでは、idのようになります。同じように、何もしないモナドが必要な場合もあります(何もしない関数が必要な場合もあります)。
Identity allows us to define just monad transformers and then define their corresponding monads just as SomeT Identity.
PS Identityの詳細については、Identityモナドが役立つ理由を参照してください。
次に、WriterTのMonad実装を見てください。
instance (Monoid w, Monad m) => Monad (WriterT w m) where
return a = writer (a, mempty)
m >>= k = WriterT $ do
~(a, w) <- runWriterT m
~(b, w') <- runWriterT (k a)
return (b, w `mappend` w')
fail msg = WriterT $ fail msg
writer :: (Monad m) => (a, w) -> WriterT w m a
writer = WriterT . return
ここで、aは値のタイプ、wは追加のMonoidのタイプです。モナド実装の観点から、左から値aと追加情報wを取り出し、右の関数をaに適用し、結果から値bと追加情報mappend
w 'を取り出し、結果値はb、追加情報はw w'、最後に、結果をreturnでラップして、タイプmの値をWriterT値コンストラクターのパラメーターとして返します。
重要な点はmappend
、ログコンテキストを保持し、操作ログの自動保守を実現するために、値を計算するときに追加情報に対してw w 'を実行することであることに注意してください。
m = Identity(Writerはこのように定義されています)の場合、特定のプロセスは次と同等です。
(WriterT (Identity (a, w))) >>= f = let (WriterT (Identity (b, w'))) = f a in WriterT (Identity (b, w `mappend` w'))
PS〜(a、w)の〜は、レイジーパターンマッチングを意味します(詳細については、Haskell / Lazingss |レイジーパターンマッチングを参照してください)。
prepending a pattern with a tilde sign delays the evaluation of the value until the component parts are actually used. But you run the risk that the value might not match the pattern — you’re telling the compiler ‘Trust me, I know it’ll work out’. (If it turns out it doesn’t match the pattern, you get a runtime error.)
やってみよう:
> runWriterT $ (return 1 :: WriterT String Identity Int)
Identity (1,"")
> let (WriterT (Identity (a, w))) = WriterT (Identity (1,"")) in (a, w)
(1,"")
> (WriterT (Identity (1,"abc")) :: WriterT String Identity Int) >>= \a -> return (a + 1)
WriterT (Identity (2,"abc"))
Writerは値コンストラクターを直接公開しませんが、Writerは、たとえば次のようにwriter関数を使用して構築できます。
> writer (1,"abc") :: Writer String Int
WriterT (Identity (1,"abc"))
さらに、それはより明確な表記法で説明することができます:
do {
a <- writer (1, "one")
b <- writer (2, "two")
return (a + b)
} :: Writer String Int
-- 得到的结果
WriterT (Identity (3,"onetwo"))
ログは互いに接着されています。代わりに配列を使用してください。
do {
a <- writer (1, ["one"])
b <- writer (2, ["two"])
return (a + b)
} :: Writer [String] Int
-- 得到的结果
WriterT (Identity (3,["one","two"]))
計算結果とログはそれから分離することができます:
> fst . runWriter $ WriterT (Identity (3,["one","two"]))
3
> snd . runWriter $ WriterT (Identity (3,["one","two"]))
["one","two"]
> mapM_ putStrLn $ snd . runWriter $ WriterT (Identity (3,["one","two"]))
one
two
上記の欠陥は完全に解決されているようです。別の質問があります。無関係なログのみを挿入したい場合はどうなりますか?
もちろんできます。tellを使用して、値なしで追加情報を挿入できます。
tell :: MonadWriter w m => w -> m ()
I / Oシナリオでの印刷と同様:
print :: Show a => a -> IO ()
効果も
同じですが、値がなくても、Writer:などの情報のみが記録されます。
logNumber :: Int -> Writer [String] Int
logNumber x = writer (x, [show x])
multWithLog :: Writer [String] Int
multWithLog = do
a <- logNumber 3
b <- logNumber 5
tell ["*"]
return (a*b)
オペランドと演算子は、同様の接尾辞式の形式で記録できます。
> multWithLog
WriterT (Identity (15,["3","5","*"]))
特定のプロセスは次と同等です。
> writer (3, ["3"]) >>= \a -> (writer (5, ["5"])) >>= \b -> (writer ((), ["*"])) >> return (a * b) :: Writer [String] Int
WriterT (Identity (15,["3","5","*"]))
実行プロセスの追跡を振り返ると、Writerのソリューションは、計算に関係する値にログコンテキストを追加し、計算プロセス中(内部マップペンドを介して)このログを維持し続けることです。これにより、計算結果のコンテキストが完成します。操作ログ:
通常の値をモナディック値に書き直し、>> =とWriterを使用してすべてを処理します。
したがって、通常の関数をログ付きのバージョンに変換する場合は、パラメーター(および操作の定数)をWriterにパックするだけです。
差分リストで
は、リストを使用してログを保持しますが、これは漠然と不安です。
Listの++操作はデフォルトで右に関連付けられている(つまり、Listの先頭に挿入されている)ため、効率が高くなります。リストの最後に頻繁に挿入する場合は、毎回左側のリストをトラバースして作成する必要があり、これは非常に非効率的です。それで、より効率的なリストはありますか?
はい、それは差分リストと呼ばれ、効率的な追加操作を実行できます。実現のアイデアは非常に賢いです:
まず、各リストを関数に変換します。
-- [1,2,3]
\xs -> [1,2,3] ++ xs
-- []
\xs -> [] ++ xs
注:重要な点は、関数本体にリストのみが付加されることです。これにより、++操作の効率が保証されます(ヘッド挿入効率が非常に高い)
当然、Listの追加操作は関数の組み合わせになります。
f = \xs -> "dog" ++ xs
g = \xs -> "meat" ++ xs
f `append` g = \xs -> f (g xs)
-- 展开f `append` g,得到
\xs -> "dog" ++ ("meat" ++ xs)
空のリストを差分リスト(リストパラメーターを受け入れる関数)に渡して、結果リストを取得します。
> f `append` g $ []
"dogmeat"
したがって、DiffListを次のように定義します。
newtype DiffList a = DiffList { getDiffList :: [a] -> [a] }
toDiffList xs = DiffList (xs++)
fromDiffList (DiffList f)
]
次に、Monoidを実現します。
instance Monoid (DiffList a) where
mempty = DiffList (\xs -> [] ++ xs)
(DiffList f) `mappend` (DiffList g) = DiffList (\xs -> f (g xs))
やってみよう:
> fromDiffList $ (toDiffList [1, 2, 3]) `mappend` (toDiffList [3, 2, 1])
[1,2,3,3,2,1]
では、パフォーマンスの違いは何ですか?単にテストする:
countdown n = if (n == 0) then do {tell ["0"]} else do {countdown (n - 1); tell [show n]} :: Writer [String] ()
countdown' n = if (n == 0) then do {tell (toDiffList ["0"])} else do {countdown' (n - 1); tell (toDiffList [show n])} :: Writer (DiffList String) ()
カウントダウンシナリオでは、ライターを使用してカウントダウンプロセスの各数値を記録します。違いは、カウントダウンではリストを使用してログを保持するのに対し、カウントダウンではDiffListを使用することです。
500,000カウントなど、ほとんどの場合、しばらくの間:
> mapM_ putStrLn . snd . runWriter $ countdown 500000
> mapM_ putStrLn . fromDiffList . snd . runWriter $ countdown' 500000
肉眼で見える効率の観点から、カウントダウンはどんどん遅くなり、カウントダウンは常にスムーズに出力されます
PSより科学的なテスト方法については、パフォーマンス| 5ベンチマークライブラリを参照してください。
PSDiffListの完全な実装については、spl / dlistを参照してください。
PSさらに、Haskell Platformにはデフォルトでdlistパッケージが付属していません(したがって、デフォルトで組み込みのDiffListはありません)。手動でインストールする必要があります。この記事の冒頭を参照してください。
2.
リーダーモナドリーダーモナドは実際には関数モナドであり、関数もモナドです。これをどのように理解しますか?
関数は、コンテキストを含むと考えることもできます。このコンテキストは、まだ表示されていない特定の値を期待していることを意味しますが、それを関数パラメーターとして使用し、関数を呼び出して結果を取得することはわかっています。
つまり、(->)r、私はそれがFunctorでありApplicativeであることを以前に知っていました。それはモナドであることが判明し、その具体的な実装は次のとおりです。
instance Monad ((->) r) where
f >>= k = \ r -> k (f r) r
returnの追加の実装はないため、純粋に適用可能です。
instance Applicative ((->) a) where
pure = const
const :: a -> b -> a
const x _ = x
任意の値(x)を受け入れ、パラメーターを受け入れ、以前に渡された任意の値を無視して返す関数(_-> x)を返します。これは、値を関数コンテキストにラップして、関数操作に参加できるようにするために行われます。
関数を特定の固定値にする唯一の方法は、そのパラメーターを完全に無視することです。
=実装の観点から、新しい関数が生成され(\ r-> k(fr)r)、パラメーター(r)を受け入れ、左側のモナディック値(関数f)に渡されます。次に、戻り値(fr)を右側の関数(k)に渡し、モナディック値を返し(関数、k(fr)のまま)、パラメーター(r)を受け入れ、最後にモナディック値を返します。
PS rをパラメータとしてfに渡すのは奇妙に思えます。これは、fがモナディック値であり、コンテキスト(関数)を持っているためです。関数コンテキストから値を取得するには、パラメータを指定する必要があります。同様に、k(fr)の場合、fからkに取得した値をフィードし、関数コンテキストで何かを返し、最後にパラメーターrをフィードして最終結果を取得します。
さて、機能はモナドになりました、それの用途は何ですか?
palyAGame x = do
f <- (/0.5) . (subtract 3.9343) . (*5) . (+52.8)
g <- (*10)
return (f - g)
心の中の数字を考えて、それを使って52.8を足し、5を掛け、次に3.9343を引き、0.5で割り、最後に必要な数の10倍を引きます
変換プロセスを見て、計算式をxに関連する2つの部分に分割し、最後に減算を行います。最初に試してみてください:
> palyAGame 201807 01
520.1314
結果はreturn _-> xでラップされるため、xに加えて、もう1つのパラメーターも入力することに注意してください。結果を取得するには、パラメーターを入力する必要があります。
FunctionMonadの実際の役割を思い出してください。
すべての関数を結合して大きな関数を作成し、この関数のパラメーターをすべての関数にフィードします。つまり、将来の値を取得します。
PS「Taketheirfuture value」は、最後のf-g、いたずらな説明を指します
実際、より科学的な説明は次のとおりです。
The Reader monad (also called the Environment monad). Represents a computation, which can read values from a shared environment, pass values from function to function, and execute sub-computations in a modified environment.
その中で、共有環境とは、変数バインディングの維持を指します。つまり、doブロック内のすべてのモナディック値は、この大きな関数のパラメーターを共有します。関数間で値を渡すことの意味は、改ざんに関しては「将来の値を取り出す」ことに似ています。過去の環境でのサブ計算は、依存関係の注入などのアプリケーションシナリオを参照する場合があります(リーダーモナドの目的は何ですか?を参照)。
PSは共有環境から値を読み取ることができるため、ReaderMonadと呼ばれます
3.
ログ追跡と共有環境に加えて、StateMonadには状態維持の最も一般的な問題もあります
ただし、問題が基本的に時間の経過に伴う状態の変化に依存している領域がいくつかあります。Haskellでそのようなプログラムを書くこともできますが、書くのが非常に難しい場合があります。これが、HaskellがStateMonad機能を追加した理由です。これにより、Haskellの状態の問題を簡単に処理し、プログラムの他の部分を純粋に保つことができます。
これがステートモナドの意味です。他の純粋なパーツに影響を与えずに、ステートのメンテナンスを簡単にしたいです。
実装の観点から、State Monadは、状態を受け入れ、値と新しい状態を返す関数です。
s -> (a,s)
-- 即
state -> (result, newState)
Writer Monadと同様に、結果の値は、コンテキスト(ここでは、newState)に添付された追加情報から分離され、2つのタプルによって編成されます。
具体的な実装は次のとおりです。
type State s = StateT s Identity
newtype StateT s m a = StateT { runStateT :: s -> m (a,s) }
instance (Monad m) => Monad (StateT s m) where
return a = StateT $ \ s -> return (a, s)
m >>= k = StateT $ \ s -> do
~(a, s') <- runStateT m s
runStateT (k a) s'
fail str = StateT $ \ _ -> fail str
returnは、受信した値をs->(a、s)の状態操作関数に入れ、それをStateTにラップします。
=左から状態操作関数を取り出し、sを渡して新しい状態s 'と計算結果aを取り出し、右側の関数を計算結果aに適用してモナディック値を取得し、runStateTを介して内部の状態操作関数を取り出します。 、新しい状態s 'に適用し、(a、s)2タプルを取得して戻ります。このように、ラムダのタイプは標準のs->(a、s)であり、最後にStateTをプラグインして新しいモナディック値を作成します。
ステートモナドは、ステートメンテナンス操作をより簡潔に表現できます。それでは、このことでステートメンテナンス操作をどの程度簡素化できるでしょうか。ランダム番号の例を見てみましょう
ランダム番号と状態モナド
シナリオに関する限り、ランダム番号は状態(ランダム番号シード)を維持する必要があります。これは、状態モナドでの処理に非常に適しています。
具体的には、以前のランダム番号シナリオでは、さまざまなランダム番号シードをランダム関数に変更することによってランダム番号が生成されました。
random :: (Random a, RandomGen g) => g -> (a, g)
例えば:
> random (mkStdGen 7) :: (Bool, StdGen)
(True,320112 40692)
3つのランダムな数値を生成するための最も直接的な方法は、次のとおりです。
random3'' = let (r1, g1) = random (mkStdGen 7); (r2, g2) = random g1; (r3, g3) = random g2 in [(r1, g1), (r2, g2), (r3, g3)] :: [(Bool, StdGen)]
> random3''
[(True,320112 40692),(False,2071543753 1655838864),(True,33684305 2103410263)]
もちろん、もう少しエレガントにカプセル化することもできます。
random3 i = collectNext $ collectNext $ [random $ mkStdGen i]
where collectNext xs@((i, g):_) = [random g] ++ xs
> reverse $ random3 7 :: [(Bool, StdGen)]
[(True,320112 40692),(False,2071543753 1655838864),(True,33684305 2103410263)]
見た目は快適ですが、それでも面倒です。ステートモナドに切り替えます。
randomSt :: (RandomGen g, Random a) => State g a
randomSt = state random
threeCoins :: State StdGen (Bool,Bool,Bool)
threeCoins = do
a <- randomSt
b <- randomSt
c <- randomSt
return (a,b,c)
非常にエレガントに見えます。ランダム関数はs->(a、s)の形式を満たすだけなので、状態:: MonadState sm =>(s->(a、s))-> maにスローして、StateMonad値を作成します。 。やってみよう:
> runState threeCoins (mkStdGen 7)
((True,False,True),33684305 2103410263)
結果(a、s)の状態sは、4番目のランダム番号シード(着信mkStdGen 7をカウント)です。これは、このシードが最新の状態であるためです(残りの中間状態は失われます)。
はい、Moandは状態維持の一般的なシナリオを簡素化します。StateMonadは、中間状態の維持を自動的に完了し、すべてを可能な限り単純にするのに役立ちます。
4.エラーモナド
最後に、例外処理も重要なシナリオであり、モナドを使用して簡略化することもできます。
Building computations from sequences of functions that may fail or using exception handling to structure error handling.
たぶん、エラーを生成する可能性のある計算を表現するために使用できるモナドであることはすでに知っています。どちらもどうですか?大丈夫ですか?
もちろん。実際、EitherはError Monad(Exception monadとも呼ばれます)のインスタンスです。
class (Monad m) => MonadError e m | m -> e where
throwError :: e -> m a
catchError :: m a -> (e -> m a) -> m a
instance MonadError e (Either e) where
throwError = Left
Left l `catchError` h = h l
Right r `catchError` _ = Right r
(Control.Monad.Exceptから)
PS Control.Monad.ErrorとControl.Monad.Trans.Errorは古くなっていることに注意してください。Control.Monad.Exceptを使用することをお勧めします。詳細については、Control.Monad.Errorを参照してください。
throwErrorについては何も言うことはありません。左xはエラーを表します(右xは通常の結果を表します)。CatchErrorを使用してエラーをキャッチできます。エラーが発生しない場合は、何もしません。したがって、一般的なパターンは次のとおりです。
do { action1; action2; action3 } `catchError` handler
例えば:
do {
x <- (Left "error occurred")
return x
} `catchError` error
エラーをキャッチし、エラーを直接スローすると、エラーが発生します。
*** Exception: error occurred
上面do block中的操作实际上依赖的是Either自身的Monad实现:
instance Monad (Either e) where
Left l >>= _ = Left l
Right r >>= k = k r
等价于:
> ((Left "error occurred") >>= (\x -> return x) :: Either String Int) `catchError` error
*** Exception: error occurred
> ((throwError "error occurred") >>= (\x -> return x) :: Either String Int) `catchError` error
*** Exception: error occurred
言い換えると、Error Monadは、煩わしい変更を加えることなく、エラーを表現できるタイプ(Either、Maybeなど)に対してのみ追加のthrowErrorとcatchErrorを実装しますが、これら2つの動作により、エラーを適切に処理できます。これは、上記で紹介したモナドとは異なります
どちらかに加えて、MonadErrorのもう1つの重要なインスタンスはExceptTです(もちろん、これら2つ以上あります)。
instance Monad m => MonadError e (ExceptT e m) where
throwError = ExceptT.throwE
catchError = ExceptT.catchE
まとめた後、ExceptTで定義されたthrow and catchを使用できるため、ExceptTは他のモナドにエラー処理機能を追加できます。実装は次のとおりです。
newtype ExceptT e m a = ExceptT (m (Either e a))
runExceptT :: ExceptT e m a -> m (Either e a)
runExceptT (ExceptT m) = m
throwE :: (Monad m) => e -> ExceptT e m a
throwE = ExceptT . return . Left
catchE :: (Monad m) =>
ExceptT e m a -- ^ the inner computation
-> (e -> ExceptT e' m a) -- ^ a handler for exceptions in the inner
-- computation
-> ExceptT e' m a
m `catchE` h = ExceptT $ do
a <- runExceptT m
case a of
Left l -> runExceptT (h l)
Right r -> return (Right r)
実際、他のモナドの値(a)をEitherにラップし、例外情報(e)を追加する一方で、モナドのタイプが正しいことを確認します(まだm)
ThrowEは、エラーメッセージをEither with Leftに変換し、それを目的のMonadにラップして返し、最後にExceptTに接続してExceptT値を作成します。
catchEは、runExceptTを介して左側を取り出し、エラーがあるかどうかを確認してから、右側のハンドラーにスローするかどうかを決定します。
すべて理解できたので、I / O操作に例外処理を追加してみてください。
getString :: ExceptT String IO String
getString = do {
line <- liftIO getLine;
if (null line) then
throwError "empty input"
else
return line
} `catchError` (\e -> return "Error occurred, use default string")
safeIO = do
-- 放心用Right匹配,因为getString有错误处理
(Right line) <- runExceptT getString
putStrLn line
liftIO :: MonadIO m => IO a-> maは、IOを必要なMonadコンテキスト(上記の例のExceptT)に上げるために使用されることに注意してください。
IOモナドから計算を解除します。
また、runExceptTは、Exceptにパッケージ化されているものを取り出すために使用されます。次に例を示します。
> runExceptT (liftIO getLine :: ExceptT String IO String)
aaa
Right "aaa"
やってみよう:
> safeIO
Error occurred, use default string
> safeIO
abc
abc
予想どおり、入力が不正な場合は、デフォルトの文字列を使用してください
PSさらに、ExceptはExceptTに基づいて定義されます。
type Except e = ExceptT e Identity
except :: Either e a -> Except e a
except m = ExceptT (Identity m)
runExcept :: Except e a -> Either e a
runExcept (ExceptT m) = runIdentity m
詳細については、Control.Monad.Trans.Exceptを参照してください。
5.
Monad Monadの魅力は、次のようないくつかの追加機能を計算に与えることができます。
Writer Monad:関数をログバージョンに変換したり、実行プロセスを追跡したり、データ変換に情報を追加したりできます。
Reader Monad:この環境からのパラメーターの読み取り、他の関数の結果の読み取りなど、一連の関数を制御可能な共有環境で連携させることができます。
状態モナド:状態を自動的に維持できます。一連のランダムな数値を生成するなど、状態を維持する必要があるシナリオに適しています。
エラーモナド:エラー処理メカニズムを提供します。これにより、計算をより安全に簡単に行うことができます。
Monadの重要性は、これらの一般的なシナリオから一般的なパターンを抽象化して、状態の保守、ログ収集などの操作を簡素化することです。これは、Monadによって自動的に完了できます。
使用の観点からは、Monadパッケージを使用するだけで(はい、それはとても簡単です)、追加の機能を取得できます。これがMonadの魅力です。
参考資料
Control.Monad.Reader
Control.Monad.Error
Control.Monad.Except
ayqyに連絡する