Abstract
Mocking is an infamous technique from object-oriented programming.
The goal is to be able to test stateful systems in small pieces by simulating the behaviour of certain objects.
The problem with mocking is that it usually requires heavyweight frameworks and clutters test code.
There are countless rants on that topic, but this talk isn't one.
Instead, we'll explore the functional approach in Haskell: Designing a small language supporting the desired behaviour, and then writing interpreters which can execute its semantics in various ways.
Testing I/O code was never easier.
Slides
Download (PDF)Recording
Events
- Lambda Days, Kraków, Poland, February 26th, 2015
- Regensburg Haskell Meetup, Regensburg, Germany, March 16th, 2015
- Oslo Socially Functional Programmers, Oslo, Norway, June 23rd, 2015
Demo code
{-# LANGUAGE GADTs #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TypeOperators #-}
module Terminal where
import Control.Monad.Free (Free, liftF, iterM)
import Control.Monad.State
import Data.Functor.Coyoneda (Coyoneda (Coyoneda), liftCoyoneda)
type FreeC f = Free (Coyoneda f)
type f ~> g = forall a. f a -> g a
runFreeC :: Monad g => (f ~> g) -> FreeC f a -> g a
runFreeC f = iterM $ \(Coyoneda g i) -> f i >>= g
liftC :: f a -> FreeC f a
liftC = liftF . liftCoyoneda
data Terminal a where
ReadLine :: Terminal String
WriteLine :: String -> Terminal ()
type TerminalIO = FreeC Terminal
readLine = liftC ReadLine
writeLine s = liftC (WriteLine s)
terminalToIO :: Terminal ~> IO
terminalToIO ReadLine = getLine
terminalToIO (WriteLine s) = putStrLn s
type MockTerminal = ([String], [String])
terminalToState :: Terminal ~> State MockTerminal
terminalToState ReadLine = do
(i, o) <- get
case i of
h : t -> do
put (t, o)
return h
[] ->
return ""
terminalToState (WriteLine s) = do
(i, o) <- get
put (i, o ++ [s])
program :: TerminalIO ()
program = do
line <- readLine
writeLine line