Settings

Theme

Unit testing IO in Haskell

blog.pusher.com

69 points by fractalsea 10 years ago · 10 comments

Reader

gamegoblin 10 years ago

This is a good post in that it illustrates how it's possible to write useful Haskell that interacts with the real world without weaving IO through the entire codebase (as many beginners end up doing).

I still find myself preferring to use a dependency injection style when writing Haskell. The code ends up looking similar, and more flexible, in my opinion (you can inject different dependencies based on runtime values).

Here is an example using the method described in the article vs. dependency injection to print out the current time.

Article:

    class MonadTime m where
        getTime :: m Integer

    class MonadPrint m a where
        doPrint :: a -> m ()

    instance MonadTime IO where
        getTime = do
            TOD s _ <- getClockTime
            return s

    instance Show a => MonadPrint IO a where
        doPrint = print

    printTime :: (Monad m, MonadPrint m Integer, MonadTime m) => m ()
    printTime = getTime >>= doPrint

    main :: IO ()
    main = printTime
Dependency injected:

    data MonadPrint m a = MonadPrint { doPrint :: a -> m () }

    data MonadTime m = MonadTime { getTime :: m Integer }

    ioMonadTime :: IO Integer
    ioMonadTime = do
        TOD s _ <- getClockTime
        return s

    printTime :: Monad m => MonadPrint m Integer -> MonadTime m -> m ()
    printTime MonadPrint{..} MonadTime{..} = getTime >>= doPrint

    main :: IO ()
    main = printTime (MonadPrint print) (MonadTime ioMonadTime)
 
    
Good read on the subject of being careful with overusing typeclasses:

http://www.haskellforall.com/2012/05/scrap-your-type-classes...

  • platz 10 years ago

    Of course typeclasses can be viewed as dependency injection as well. It its just that with typeclasses the injection is implicit rather than explicit. But mechanically they are identical

    • fractalseaOP 10 years ago

      This is what I thought when I first saw the parent comment. It looks like an interesting alternative approach, but from the example I am not clear on why it is more flexible than the version using typeclasses.

      I reckon I will need to re-implement my mocks using the technique you describe to really understand the motivation.

      • gamegoblin 10 years ago

        I didn't provide an example of the flexibility for sake of brevity.

        The key point I want to make with regard to flexibility is the ability to have multiple "instances" and switch between them based on run-time values.

        For a trivial example, consider searching an ordered array. Let me define the typeclass:

            class Container a where
                type Item a
                size :: a -> Int
                contains :: Item a -> a -> Bool
        
        Now consider that I have the type SortedArray that I want to make an instance of Container.

            instance Container (SortedArray a) where
                type Item (SortedArray a) = a
                size = sortedArraySize
                contains = ???
        
        What should my `contains` be? I could linear search or I could binary search. Depending on the size of the array, either could be faster (due to cache performance and constant overheads). For small arrays, linear search could be faster, and for large arrays, binary search could be faster.

        I could write my method like this:

            contains x xs = if size xs <= 64
                then linearSearch x xs
                else binarySearch x xs
        
        But having that 64 hard coded in is sort of lame. Suppose I am writing software that will run on unknown hardware. I don't know the cache properties of it. Perhaps 16 is better. Perhaps 256. Since my application could be super high performance, I need it to be nearly optimal.

        So on application start-up, I call a function which instantiates arrays of varying sizes and tries linear search vs. binary search to find the point at which one outperforms the other.

        How can I use this value? I can't inject it in place of that 64 above (without unsafePerformIO...).

        The solution is to instead represent the typeclass at the value level:

            data Container a b = Container
                { size :: a -> Int
                , contains :: b -> a -> Bool
                }
        
        Now I can have code that looks like:

            main = do
                n <- findOptimalSearchSplitPoint
        
                let sortedArraySearch x xs = if sortedArraySize xs <= n
                        then linearSearch x xs
                        else binarySearch x xs
                let container = Container sortedArraySize sortedArraySearch
        
        Then I just inject that container "instance" wherever it needs to go.
    • gamegoblin 10 years ago

      The main difference is that typeclasses are injected at compile time rather than run time. By being explicit, I can inject different behavior based on runtime values.

      • platz 10 years ago

        well, the typeclasses are 'determined' at compile-time, but they are certainly de-sugared into extra function paramaters that are still passed around (injected) at run-time. Haskell does not completely specialize this away (even though in theory it could). But i grant you manual threading allows the user to do different things than expected.

        • tome 10 years ago

          Not all typeclass instances can be determined at compile time!

LukeHoersten 10 years ago

Pusher is doing some awesome stuff with Haskell! They run a lot of the bitcoin exchange web socket APIs on their infrastructure. I'm sure they have other users too. Cool stuff.

bartq 10 years ago

finally someone did haskell examples in decent modern blog UI instead of old fashioned dark depressing terminal-like colors ;)

Keyboard Shortcuts

j
Next item
k
Previous item
o / Enter
Open selected item
?
Show this help
Esc
Close modal / clear selection