Sandbox

Haskell

Characteristics

Scope: Structural Safety: Safe

Explanation

That computer sitting on your desk is not equivalent to a Turing Machine. This shouldn't be surprising, since Turing Machines are a mathematical model hat would be pretty much useless on practice. They are able to do any calculation, but only one calculation per machine, and can not do anything with the results. Instead, your computer interacts with the world, and Haskell, being a pure language explicits that interaction as the IO monad.

And you probably already knew all that... You also may, or may not have thought that IO is a very powerful monad, in fact too powerful, and it would be cool if one could factor its capacities into less powerful ones you can use around your code. This way, a function with some FileWriteCapacity couldn't connect into a web server, or create a window at your screen.

Well, turns out that creating those monads is possible, but it's a hell of a lot of work, and they have just too little power for being useful. Yet, you can easily make something even better!

I don't remember where I first saw this pattern, but it is very simple. You define a type class for the desired capacity, like:

  import qualified Data.Time.Clock as Time

  class Monad m => GetTimeCapacity m where
    currentTime :: m Time.UTCTime

And, well, I'm not sure why everybody always use the time example, but I have this exact class on my game, so I think it's useful. So, let's move on. Then, you create instances for it, like:

  instance GetTimeCapacity IO where
    currentTime = Time.getCurrentTime

And you can create functions with this capacity be declaring them as:

  usesTime :: GetTimeCapacity m => a -> m b
  usesTime = -- the implementation

And you get a function that you may call from IO like any other, but is guaranteed to only use the current time, nothing else.

But it does not stop there, now that you created that class, you can write something like:

  newtype TimeTester = TimeTester (Reader Time.UTCTime)
    deriving (Applicative, Monad, ...)
  instance GetTimeCapacity Tester where
    currentTime = ask

And call your function on this monad to test code that uses it. Or you can create a monad that keeps track of some network level logic time, or whatever.

Interaction

This pattern works beautifully together with the Swiss-Army Knife <swiss-army-knife>_ one. Just keep in mind that while the swiss-army pattern begs for overlapping instances, this one begs for orphan instances, and you can't have both.

I'd recommend keeping the overlapping instances, and newtype all the orphan instances you'd tempted to define. NewtypeDeriving will make your life easier.