先日 GHCi で :i (,)
したら Monad
のインスタンス定義があることに気づいた。
instance Monoid a => Monad ((,) a) -- Defined in ‘GHC.Base’
ドキュメントによるとどうやら base-4.9.0.0
から定義されているらしい。そうだよね。昔はなかった気がするもんね。
でもタプルにモナドインスタンスの定義があるからといって特に驚くべきものというわけでもない。このモナドはもうみんな知っているはずだ。 Writer
である。
newtype Writer w a = Writer { runWriter :: (a, w) }
instance (Monoid w) => Monad (Writer w)
モナド副作用としてログ値を隠し持つためにタプルの片方を使おうというわけだ。 Writer
の定義では fst
側が値、 snd
側がログ値だが、タプルそのものに定義されているものはこれが逆で、 fst
側がログ値、 snd
側が値となっているらしい。
というわけで fst
側にログ値を置けばそれがモナド裏に隠し持たれ、最後に全部 mappend
されてくっついたものが得られる。ちょっと使ってみよう。
main :: IO ()
main = print $ do
(["log1"], ())
x <- (["log2"], 2)
y <- (["log3"], 3)
return $ x + y
(["log1","log2","log3"],5)
<-
の右にタプルそのものが現れるので do
構文としてはちょっと珍しい形になるのが面白い。とはいうものの見た目の話ならリストモナドも似たようなものか。Writer
でいうところの tell log
は (log, ())
にあたる。 return value
は (mempty, value)
である。
もう少し実用的に使えそうな例も考えてみよう。例えば ByteString
を先頭から少しずつ切り取っていくことを考える。最初の 2 bytes は固定の値が入っていて、次の 2 bytes はバージョン番号で、その次の 4 bytes はファイルサイズで…、なんてのはありそうな話だろう1。愚直に書くとこんな感じになる。
import Data.ByteString.Lazy (ByteString)
import qualified Data.ByteString.Lazy as LB
import qualified Data.ByteString.Lazy.Char8 as C
parseFile :: ByteString -> ([ByteString], ByteString)
parseFile bs = ([a, b, c], bs''')
where
(a, bs') = LB.splitAt 2 bs
(b, bs'') = LB.splitAt 2 bs'
(c, bs''') = LB.splitAt 4 bs''
main :: IO ()
main = print . parseFile $ C.pack ['A'..'Z']
(["AB","CD","EFGH"],"IJKLMNOPQRSTUVWXYZ")
ううむ。なんだろう、この bs'''
てのは。別に bs1
bs2
... でも良いのだけど、どちらにせよとても野暮ったい感じがする。この例のように三つくらいならまだ良いけど、数が増えていくたびにつらい気持ちになるはずだ。
そこでタプル自身がモナドであることを利用してこれをちょっと書き換えてやる。
-- 他の import と main は前の例と同じ
import Control.Arrow (first)
parseFile :: ByteString -> ([ByteString], ByteString)
parseFile bs =
split 2 bs
>>= split 2
>>= split 4
where
split n = first pure . LB.splitAt n
ふむふむ。これで野暮ったい変数名を付けていく必要がなくなって多少マシになった。とはいえこれでも数が増えれば似たような行がずっと並ぶわけで、それももうちょっとどうにかしたいよね。
うーん。この >>=
が連続するパターンはどこかで見たことがあるぞ。そうだ、これは foldM
だ。
-- 他の import と main は前の例と同じ
import Control.Monad (foldM)
parseFile :: ByteString -> ([ByteString], ByteString)
parseFile bs = foldM split bs [2, 2, 4]
where
split b n = first pure $ LB.splitAt n b
これで同じ結果が得られる。コードとしては大分すっきりしたのではなかろうか2。