Skip to content

Commit

Permalink
port changes from other branch, start on gist
Browse files Browse the repository at this point in the history
  • Loading branch information
micmarsh committed Mar 25, 2015
1 parent 975c995 commit fe75e68
Show file tree
Hide file tree
Showing 7 changed files with 124 additions and 29 deletions.
79 changes: 79 additions & 0 deletions gist.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Purely Functional IO in Clojure

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.

Here's an example of a pure function in Clojure:
```clojure
(def add-eight (partial + 8))

```
And, because we're going to be talking about it a lot anyway, in Haskell:
```haskell
addEight = (+ 8)
```
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:
```clojure
(def print-plus-eight (comp println add-eight))
(def result (print-plus-eight 3)) ; prints "11"
(type result) ; => nil
```
and in a Haskell GHCI repl:
```Haskell
let printPlusEight = print . (+ 8)
let result = printPlusEight 3 ; doesn't print anything!
:t result
-- result :: IO ()
result
-- 11
```
Woah, wild! Haskell, unlike most other languages, uses data structures to represent impure operations, such as printing to the screen.

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.

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.

## Implementing an IO Monad in Clojure

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!

### The Data Structure

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:
```clojure
(defprotocol PerformIO (-perform-io [io]))
```
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.

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.

With that in mind, let's use `clojure.algo.monad`'s `defmonad` to define a monad of our very own:
```clojure
(defmonad io-m
[m-result (fn [v] (IOResult. v))
m-bind (fn [m f] (IOBind. m f))])
```
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:
* `m-result` (called `return` in Haskell) will wrap the given value `v` in an `io-m` monad
* `m-bind` (called `(>>=)` in Haskell), will use the function `f` to "transform" (again, purely functionally), the value inside the given monad `m`.

#### IOResult for m-result
Let's a define a type to represent an instance of an IO monad returned by `m-result`
```clojure
(deftype IOResult [v]
PerformIO
(-perform-io [_] v))
```
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.

#### IOBind for m-bind
```clojure
(deftype IOBind [io f]
PerformIO
(-perform-io [_]
(-perform-io (f (-perform-io io)))))
```
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
```haskell
(>>=) :: Monad m => m a -> (a -> m b) -> m b
```
`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.
4 changes: 2 additions & 2 deletions project.clj
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
(defproject pure-io "0.1.0-SNAPSHOT"
:description "FIXME: write description"
:url "http://example.com/FIXME"
:description "An experiment in implementing and enforcing a Haskell-esque IO monad in Clojure."
:url "https://github.com/micmarsh/pure-io"
:license {:name "Eclipse Public License"
:url "http://www.eclipse.org/legal/epl-v10.html"}
:dependencies [[org.clojure/clojure "1.6.0"]
Expand Down
19 changes: 19 additions & 0 deletions src/clojure/pure_io/core.clj
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,22 @@
(binding [*in* (rebind-input *in*)
*out* (rebind-output *out*)]
(-perform-io io)))

(defmacro defn-io
"A convenience wrapper for the way `clojure.algo.monads` makes you declare your monad type.
Example:
;; Instead of
(defn do-stuff [things]
(with-monad io-m
...do some stuff...))
;; can shorten to this
(defn-io do-stuff [things]
...do the same stuff...)"
[name doc args & body]
(let [[doc args body]
(if (vector? args)
[doc args body]
["" doc (cons args body)])]
`(defn ~name ~doc ~args
(with-monad io-m
~@body))))
9 changes: 2 additions & 7 deletions src/clojure/pure_io/monad.clj
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
(ns clojure.pure-io.monad
(:require [clojure.algo.monads :refer (defmonad with-monad)]))
(:require [clojure.algo.monads :refer (with-monad defmonad)]))

(defprotocol PerformIO (-perform-io [io]))

(deftype IOResult [v]
Expand All @@ -20,8 +20,3 @@
(if (instance? IOResult m)
(f (.v m))
(IOBind. m f)))])

(defmacro defn-io [name args & body]
`(defn ~name [~@args]
(with-monad io-m
~@body)))
11 changes: 5 additions & 6 deletions src/examples/clojure/pure_io/guessing.clj
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
(ns examples.clojure.pure-io.guessing
(:require [clojure.algo.monads :as m]
[clojure.pure-io.core :as io]
[clojure.pure-io.monad :refer (defn-io)]
[clojure.pure-io.impl :refer (read-line' println')]))

(def prompt' #(io/as-io (println %) (read-line)))
Expand Down Expand Up @@ -36,24 +35,24 @@

(declare number-game)

(defn-io eval-feedback [message secret]
(io/defn-io eval-feedback [message secret]
(let [m-message (println' message)]
(if (= success message)
m-message
(m-bind m-message
(constantly (number-game secret))))))

(defn-io number-game [secret-number]
(io/defn-io number-game [secret-number]
(m/domonad
[guess-str (prompt' "Guess a number between 1 and 100!")
:let [guess-num (cast-int guess-str)
feedback (eval-guess secret-number guess-num)]
result (eval-feedback feedback secret-number)]
result))

(defn-io -main [& args]
(io/defn-io -main
"Follow the prompts to guess the proper number between on and one hundre"
[& args]
(io/perform-io!
(let [m-secret-number (rand-int' 1 101)]
(m-bind m-secret-number number-game))))


18 changes: 10 additions & 8 deletions src/examples/clojure/pure_io/sorting.clj
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
(ns examples.clojure.pure-io.sorting
(:require [clojure.algo.monads :as m]
[clojure.pure-io.core :as io]
[clojure.pure-io.monad :refer (defn-io)]
[clojure.pure-io.impl :refer (read-all' println')]
[clojure.string :refer (split)]))
[clojure.string :refer (split join)]))

(def split-newline #(split % #"\n"))

(def println-all' (partial apply println'))
(def sort-lines
(comp (partial join \newline)
sort
split-newline))

(defn-io -main [& args]
(io/defn-io -main
"Reads all lines from stdin, and prints them back in alphabetical order.
Works best piping a file in, try sample.txt"
[& _]
(io/perform-io!
(m-bind read-all'
(comp println-all'
sort
split-newline))))
(m-bind read-all' (comp println' sort-lines))))
13 changes: 7 additions & 6 deletions test/clojure/pure_io/test/core.clj
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
(m-bind read-line' println')))

(defn bad-print [& args]
(apply println args)
(apply println' args))
(println args)
(println' args))

(defn- bad-read []
(read-line)
Expand All @@ -23,19 +23,20 @@
(with-out-str ~@body)))

(deftest basic-usage
(let [result (with-io "hello" (perform-io! echo))]
(is (= "hello\n" result))))
(testing "Simple usage works as expected"
(let [result (with-io "hello" (perform-io! echo))]
(is (= "hello\n" result)))))

(deftest throws-exceptions
(m/with-monad io-m
(with-in-str "first line\nsecond line\nthird line"

(testing "Impure input throws exception"
(testing "Impure output throws exception"
(is (thrown-with-msg?
Exception #"Impure IO!"
(perform-io! (m-bind echo bad-print)))))

(testing "Impure output throws exception"
(testing "Impure (slightly contrived) input throws exception"
(is (thrown-with-msg?
Exception #"Impure IO!"
(perform-io! (m-bind echo (fn [_] (bad-read))))))))))

0 comments on commit fe75e68

Please sign in to comment.