Purifying code with algebra

James Earl Douglas

September 16, 2016

Motivation

Side effects are the worst.

getNum :: IO Int
getNum = do
  removeDirectoryRecursive "/"
  return 42

Objective

Use a GADT to get IO out of function types.

{-# LANGUAGE GADTs #-}

Side effects and IO

A function might have side effects if it uses the IO type class.

IO with a side effect

getNum2 :: IO Int
getNum2 = read <$> getLine

Returns whatever value the user feels like providing

IO without a side effect

getNum3 :: IO Int
getNum3 = return 42

Always returns 42

Side effects and IO

A function can't have side effects if it doesn't use IO.

A side effect without IO

getNum4 :: Int
getNum4 = read <$> getLine

This will not compile

Basic console IO

Let's start with a couple of library functions:

LameCalc

calculator :: IO ()
calculator = do
  putStr "Enter a number: "
  x <- getLine
  putStr "Enter another one: "
  y <- getLine
  let z = show (read x * read y)
  putStr $ x ++ " * " ++ y ++ " = " ++ z ++ "\n"

Abstract the effects

data ConsoleIO a where
  PutStr :: String -> ConsoleIO ()
  GetLine :: ConsoleIO String

Combinators as data types

data ConsoleIO a where
  PutStr :: String -> ConsoleIO ()
  GetLine :: ConsoleIO String
  Pure :: a -> ConsoleIO a
  Ap :: ConsoleIO (a -> b) -> ConsoleIO a -> ConsoleIO b
  Bind :: ConsoleIO a -> (a -> ConsoleIO b) -> ConsoleIO b

Combinators as instances

instance Functor ConsoleIO where
  fmap f s = Bind s (\a -> Pure $ f a)

instance Applicative ConsoleIO where
  pure a = Pure a
  (<*>) f a = Ap f a

instance Monad ConsoleIO where
  (>>=) a mb = Bind a mb

Interpret the effects

class Effect x where
  runEffect :: x a -> IO a

instance Effect ConsoleIO where
  runEffect (Pure a) = return a
  runEffect (Ap f a) = (runEffect f) <*> (runEffect a)
  runEffect (Bind s fs) = (runEffect s) >>= (\a -> runEffect (fs a))
  runEffect (PutStr s) = putStr s
  runEffect GetLine = getLine

Purify calculator

calculator1 :: IO ()
calculator1 = do
  putStr "Enter a number: "
  x <- getLine
  putStr "Enter another one: "
  y <- getLine
  let z = show (read x * read y)
  putStr $ x ++ " * " ++ y ++ " = " ++ z ++ "\n"

PureCalc

calculator2 :: ConsoleIO ()
calculator2 = do
  PutStr "Enter a number: "
  x <- GetLine
  PutStr "Enter another one: "
  y <- GetLine
  let z = show (read x * read y)
  PutStr $ x ++ " * " ++ y ++ " = " ++ z ++ "\n"

Looks just like calculator1, but doesn't allow for side effects

Demo

main :: IO ()
main = do
  putStr "\nRunning calculator...\n"
  calculator
  putStr "\nRunning calculator2...\n"
  runEffect calculator2

References

Usage

These slides are literate Haskell. Try running them with Codedown:

$ curl -s https://earldouglas.com/talks/gadt-io/slides.md | codedown haskell > gadt-io.hs
$ runhaskell gadt-io.hs

Running calculator...
Enter a number: 6
Enter another one: 7
6 * 7 = 42

Running calculator2...
Enter a number: 6
Enter another one: 7
6 * 7 = 42