[Type class and monad madness Bryan O'Sullivan **20080411053414] { addfile ./examples/ch16/monadwriter.ghci addfile ./examples/ch16/monadHandle.ghci hunk ./en/ch15-monads.xml 310 - + hunk ./en/ch15-monads.xml 694 + + + The writer monad + + Our Logger monad is a specialised version of + the standard Writer monad, which can be found in + the Control.Monad.Writer module of the + mtl package. We will present a + Writer example in . + hunk ./en/ch16-monad-case.xml 926 - HandleIO. This would be our usual strategy in - production code. The only reason we didn't do this was to - introduce MonadIO after the initial - concepts. + HandleIO. In fact, in production code, this + would be our usual strategy. We avoided that here simply to + separate the presentation of the earlier material from that + of MonadIO. hunk ./en/ch16-monad-case.xml 933 - + hunk ./en/ch16-monad-case.xml 936 - Write me! + The disadvantage of hiding IO in another + monad is that we're still tied to a concrete implementation. + If we want to swap HandleIO for some other monad, + we must change the type of every action that uses + HandleIO. + + As an alternative, we can create a type class that + specifies the interface we want from a monad that manipulates + files. + + &MonadHandle.hs:MonadHandle; + + Here, we've chosen to abstract away both the type of the + monad and the type of a file handle. To satisfy the type + checker, we've added a functional dependency: for any instance + of MonadHandle, there is exactly one handle type + that we can use. When we make the IO monad an + instance of this class, we use a regular + Handle. + + &MonadHandle.hs:IO; + + Because any MonadHandle must also be a + Monad, we can write code that manipulates files + using normal &do; notation, without caring what monad it will + finally execute in. + + &MonadHandle.hs:safeHello; + + Because we made IO an instance of this type + class, we can execute this action from &ghci;. + + &monadHandle.ghci:IO; + + The beauty of the type class approach is that we can swap + one underlying monad for another without touching our code at + all, as our code doesn't know or care about the + implementation. For instance, we could replace + IO with a monad that compresses files as it + writes them out. + + + + Isolation and testing + + In fact, because our safeHello + function doesn't use the IO type, we can even use + a monad that can't perform I/O. This + allows us to test code that would normally have side effects + in a completely pure, controlled environment. + + To do this, we will create a monad that doesn't perform + I/O, but instead logs every file-related event for later + processing. + + &MonadHandle.hs:Event; + + Although we already developed a Logger type + in , here we'll use the + standard, and more general, Writer monad. Like + other mtl monads, the API provided by + Writer is defined in a type class, in this case + MonadWriter. Its most useful method is + tell, which logs a value. + + &monadwriter.ghci:tell; + + The values we log can be any Monoid. Since + the list type is a Monoid, we'll log to a list of + Event. + + We could make Writer [Event] an instance of + MonadHandle, but it's cheap, easy, and safer to + make a special-purpose monad. + + &MonadHandle.hs:WriterIO; + + Our execution function simply removes the &newtype; + wrapper we added, then calls the normal Writer + monad's execution function. + + &MonadHandle.hs:runWriterIO; + + When we try this code out in &ghci;, it gives us a log of + the function's file activities. + + &monadHandle.ghci:Writer; + + + + + Arbitrary I/O revisited + + If we use the type class approach to restricting + IO, we may still want to retain the ability to + perform arbitrary I/O actions. We might try adding + MonadIO as a constraint on our type class. + + &MonadHandle.hs:tidierHello; + + This approach has a problem, though: the added + MonadIO constraint loses us the ability to test + our code in a pure environment, because we can no longer tell + whether a test might have damaging side effects. The + alternative is to move this constraint from the type class, + where it infects all functions, to only those + functions that really need to perform I/O. + + &MonadHandle.hs:tidyHello; + + We can use pure property tests on the functions that lack + MonadIO constraints, and traditional unit tests + on the rest. + + Unfortunately, we've substituted one problem for another: + we can't invoke code with both MonadIO and + MonadHandle constraints from code that has the + MonadHandle constraint alone. If we find that + somewhere deep in our MonadHandle-only code, we + really need the MonadIO constraint, we must add + it to all the code paths that lead to this point. + + Allowing arbitrary I/O is risky, and has a profound effect + on how we develop and test our code. When we have to choose + between being permissive on the one hand, and easier reasoning + and testing on the other, we usually opt for the + latter. + + + + Exercises + + + + + Using QuickCheck, write a test for an action in the + MonadHandle monad, to see if it tries to + write to a file handle that is not open. Try it + out on safeHello. + + + + + Write an action that tries to write to a file handle + that it has closed. Does your test catch this + bug? + + + hunk ./examples/ch16/MonadHandle.hs 11 -import Control.Monad.Trans +import Control.Monad.Trans (MonadIO(..)) hunk ./examples/ch16/MonadHandle.hs 13 + +{-- snippet MonadHandle --} hunk ./examples/ch16/MonadHandle.hs 17 +import System.Directory (removeFile) hunk ./examples/ch16/MonadHandle.hs 23 +{-- /snippet MonadHandle --} hunk ./examples/ch16/MonadHandle.hs 25 +{-- snippet IO --} hunk ./examples/ch16/MonadHandle.hs 30 +{-- /snippet IO --} hunk ./examples/ch16/MonadHandle.hs 32 +{-- snippet safeHello --} hunk ./examples/ch16/MonadHandle.hs 38 +{-- /snippet safeHello --} + +{-- snippet tidyHello --} +tidyHello :: (MonadIO m, MonadHandle h m) => FilePath -> m () +tidyHello path = do + safeHello path + liftIO (removeFile path) +{-- /snippet tidyHello --} + +{-- snippet tidierHello --} +class (MonadHandle h m, MonadIO m) => MonadHandleIO h m | m -> h + +instance MonadHandleIO System.IO.Handle IO + +tidierHello :: (MonadHandleIO h m) => FilePath -> m () +tidierHello path = do + safeHello path + liftIO (removeFile path) +{-- /snippet tidierHello --} hunk ./examples/ch16/MonadHandle.hs 58 +{-- snippet Event --} hunk ./examples/ch16/MonadHandle.hs 63 +{-- /snippet Event --} hunk ./examples/ch16/MonadHandle.hs 65 +{-- snippet WriterIO --} hunk ./examples/ch16/MonadHandle.hs 68 +{-- /snippet WriterIO --} hunk ./examples/ch16/MonadHandle.hs 70 +{-- snippet runWriterIO --} hunk ./examples/ch16/MonadHandle.hs 73 +{-- /snippet runWriterIO --} hunk ./examples/ch16/monadHandle.ghci 1 +--# IO +safeHello "hello to my fans in domestic surveillance" +removeFile "hello to my fans in domestic surveillance" + +--# Writer +:load MonadHandle +runWriterIO (safeHello "foo") + +--# combined +tidyHello "much nicer" hunk ./examples/ch16/monadwriter.ghci 1 +--# tell +:m +Control.Monad.Writer +:type tell }