|
| 1 | +# Purely Functional IO in Clojure |
| 2 | + |
| 3 | +Clojure is an excellent functional programming language (I repeat myself), but it's not *pure*. Purity means every function returns the same result given the same input, just like mathematical equations. |
| 4 | + |
| 5 | +Here's an example of a pure function in Clojure: |
| 6 | +```clojure |
| 7 | +(def add-eight (partial + 8)) |
| 8 | + |
| 9 | +``` |
| 10 | +And, because we're going to be talking about it a lot anyway, in Haskell: |
| 11 | +```haskell |
| 12 | +addEight = (+ 8) |
| 13 | +``` |
| 14 | +No matter what we pass to each function, we'll always get the same result (or an exception, in Clojure's case). Now, let's add some impurity to the mix: |
| 15 | +```clojure |
| 16 | +(def print-plus-eight (comp println add-eight)) |
| 17 | +(def result (print-plus-eight 3)) ; prints "11" |
| 18 | +(type result) ; => nil |
| 19 | +``` |
| 20 | +and in a Haskell GHCI repl: |
| 21 | +```Haskell |
| 22 | +let printPlusEight = print . (+ 8) |
| 23 | +let result = printPlusEight 3 ; doesn't print anything! |
| 24 | +:t result |
| 25 | +-- result :: IO () |
| 26 | +result |
| 27 | +-- 11 |
| 28 | +``` |
| 29 | +Woah, wild! Haskell, unlike most other languages, uses data structures to represent impure operations, such as printing to the screen. |
| 30 | + |
| 31 | +In the Clojure example, `result` was of type `nil` because our function already printed and returned nothing, but in the Haskell example, our function just returned something of type `IO ()`, which means it's an IO action that hasn't actually happened yet, in that case printing "11" to the screen. |
| 32 | + |
| 33 | +Haskell's type system makes this concept especially pervasive: every Haskell program has a `main` value, which must be something of type `IO ()`. If you need to do any impure operation, you'll need to compose your data structures so it all comes together at `main`. The static type checker will ensure that everything is in order before your program ever even compiles. |
| 34 | + |
| 35 | +## Implementing an IO Monad in Clojure |
| 36 | + |
| 37 | +With the above knowledge in mind, the natural next question to is: how can we do this in Clojure? If you're as geeked out by this stuff as I am, read on! |
| 38 | + |
| 39 | +### The Data Structure |
| 40 | + |
| 41 | +Since we'll be passing around some data, but eventually using it to peform an actual IO operation, it makes sense to define a protocol for our data types to implement: |
| 42 | +```clojure |
| 43 | +(defprotocol PerformIO (-perform-io [io])) |
| 44 | +``` |
| 45 | +Now that we have that, we can define some types, but first we need to attempt the world's shortest (and probably most incomplete) explaination of Monads. |
| 46 | + |
| 47 | +Monads are, for our purposes, a "container" type for another value. Slightly more specifically, a monad is a monad as opposed to some other type because it defines two operations: one for "wrapping" values in a new instance of that monad, and another for "transforming" (in a pure, functional way), the value inside the monad into a new value. |
| 48 | + |
| 49 | +With that in mind, let's use `clojure.algo.monad`'s `defmonad` to define a monad of our very own: |
| 50 | +```clojure |
| 51 | +(defmonad io-m |
| 52 | + [m-result (fn [v] (IOResult. v)) |
| 53 | + m-bind (fn [m f] (IOBind. m f))]) |
| 54 | +``` |
| 55 | +Althought I've deferred the actual work of the functions to two custom Clojure/Java types (which we'll get to in a second), you can see the two operations defined here: |
| 56 | +* `m-result` (called `return` in Haskell) will wrap the given value `v` in an `io-m` monad |
| 57 | +* `m-bind` (called `(>>=)` in Haskell), will use the function `f` to "transform" (again, purely functionally), the value inside the given monad `m`. |
| 58 | + |
| 59 | +#### IOResult for m-result |
| 60 | + Let's a define a type to represent an instance of an IO monad returned by `m-result` |
| 61 | +```clojure |
| 62 | +(deftype IOResult [v] |
| 63 | + PerformIO |
| 64 | + (-perform-io [_] v)) |
| 65 | +``` |
| 66 | +As you can see, when `-perform-io` is called on something of this type, all it does is return the value passed into it. As a reminder, `-perform-io`, is NOT part of the definition of a monad, it's just something special we want to be able to do with our IO data. Either way, this type is pretty boring, so let's move on. |
| 67 | + |
| 68 | +#### IOBind for m-bind |
| 69 | +```clojure |
| 70 | +(deftype IOBind [io f] |
| 71 | + PerformIO |
| 72 | + (-perform-io [_] |
| 73 | + (-perform-io (f (-perform-io io))))) |
| 74 | +``` |
| 75 | +Much more exciting! To help figure out what's going on here, let's look at the Haskell type signature of `>>=`, which we'll call `m-bind` in Clojure |
| 76 | +```haskell |
| 77 | +(>>=) :: Monad m => m a -> (a -> m b) -> m b |
| 78 | +``` |
| 79 | +`Monad m =>` is saying "m must be a monad in this type signature", and `a` and `b` can be of any types whatsoever. They could even be the same, but they don't have to be. |
0 commit comments