Skip to content

Instantly share code, notes, and snippets.

@AyeGill
Last active August 29, 2015 14:16
Show Gist options
  • Save AyeGill/9498b41fc956f1aeda5d to your computer and use it in GitHub Desktop.
Save AyeGill/9498b41fc956f1aeda5d to your computer and use it in GitHub Desktop.
Intro to IO, mutation, monads, haskell, oh my!

So, haskell is a "Pure Functional" language, which basically means that functions behave(almost) like mathematical functions, rather than how they behave in other languages. So in most other languages a function can do whatever before it returns, print some text, wipe the root partition, launch nukes against China, whatever. In haskell, that's (almost) not allowed: a function takes an argument, returns an argument, and that's it.

So how do you accomplish anything? Software has to actually do stuff, so how do you interact with anything? To explain that, we need a brief interlude to explain types in haskell. First there are simple types like int, string, so on. Just like a type in C. Then you have parametric types, like List. So you can have a List int, or a List string, and that's a linked list of integers or strings. Lastly, we need to explain parametric types. For example, you might have a function that takes the length of a list. The thing about that function is that it actually doesn't care about the individual elements of the list. It works on any list. So it has the type List a -> int, where a is what we call a "type variable" - it can take on any type. We could also have a "reverse list" function, which would have the type List a -> List a. It still doesn't care about what the list actually contains, but it'll always return a list with the same type of elements as the input list.

Now, there's a type in haskell called IO. IO is a parametric type, and the meaning of IO a is basically "some sort of IO action that, if you perform it, will give you back something of type a". So, built into the language, you have readLine, which has type IO String, and reads a line from stdin, or putStrLn, which has type String -> IO (), which is a function that takes a string, and returns an IO action that will print that string when executed. Now, all haskell programs have to contain a value called main (like the main function in C), which has type IO ()(much like in C, i believe giving main the wrong return type is just a warning). When the program is run, it just runs the "main" IO action. So, to write programs, all we need is a strong enough set of primitive IO actions, and a strong enough set of combinators to build more complex IO actions. It turns out that we only need two combinators:

return :: a -> IO a, which is a function that takes any value, and makes an IO action that doesn't do anything, then returns that value.

(>>=) :: IO a -> (a -> IO b) -> IO b, often referred to as "bind". It takes an IO action that returns something of type a, and a function from things of type a to IO actions. The resulting IO action will: perform the first action, put the value into the function, then perform the resulting action and return the value.

(The reason bind looks so weird is that it's an infix operator. The left operand becomes the first argument, the right becomes the second)

(Also, bind is taking two arguments using currying. Look it up)

And that's all we need. Haskell includes something called "do syntax", which looks like this:

do 
  x <- getFirstVal
  y <- getSecondVal
  z <- performActionBasedOn(x, y)
  print z

But this can be expressed in terms of those two operators, if you're very stubborn.

Another thing you may want, that other languages handle with mutation, is the idea of a process that handles "state". So we have another type: State s a, which is "an action that mutates some state, which has type s, and returns an a in the process. This can be represented by s -> (s, a). It turns out that both state and IO are instances of a general pattern called a "monad" (it's a math thing, don't ask). Which means that we can use return and bind for state actions, too. They get the type return :: a -> State s a, and (>>=) :: State s a -> (a -> State s b) -> State s b

(In general, if m is a monad, they have the types return :: a -> m a and (>>=) :: m a -> (a -> m b) -> m b, but that's getting into type classes, which is another topic)

This means that we can also use do syntax to handle mutation. So if we have some type "world" that we're using to represent the state of our game world, we can do:

do
  let ammo = getAmmo player
  let hits = hitScan (getAim player) world
  applyDamage (getDamage player) hits
```
(let allows you to introduce local bindings without actually executing IO/mutating the state)
(here, hits would need to have a type that allows for the possibility that nothing was hit, and applyDamage would have to check for this).

And it'll work just like similar imperative code. The peril of this, of course, is that you're basically back in imperative land. But the fact that things that mess with the world state are clearly marked as such. We know(and the compiler ensures) that `getAmmo` will never, ever mess with the world state. Another thing is that, since each state mutating function just takes and returns a world object, it's easy(easier) to make sure that it always leaves the world in a "valid" state, and that if it gets a valid world, it knows what to do with it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment