Skip to content

Commit fe75e68

Browse files
committed
port changes from other branch, start on gist
1 parent 975c995 commit fe75e68

File tree

7 files changed

+124
-29
lines changed

7 files changed

+124
-29
lines changed

gist.md

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
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.

project.clj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
(defproject pure-io "0.1.0-SNAPSHOT"
2-
:description "FIXME: write description"
3-
:url "http://example.com/FIXME"
2+
:description "An experiment in implementing and enforcing a Haskell-esque IO monad in Clojure."
3+
:url "https://github.com/micmarsh/pure-io"
44
:license {:name "Eclipse Public License"
55
:url "http://www.eclipse.org/legal/epl-v10.html"}
66
:dependencies [[org.clojure/clojure "1.6.0"]

src/clojure/pure_io/core.clj

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,22 @@
3838
(binding [*in* (rebind-input *in*)
3939
*out* (rebind-output *out*)]
4040
(-perform-io io)))
41+
42+
(defmacro defn-io
43+
"A convenience wrapper for the way `clojure.algo.monads` makes you declare your monad type.
44+
Example:
45+
;; Instead of
46+
(defn do-stuff [things]
47+
(with-monad io-m
48+
...do some stuff...))
49+
;; can shorten to this
50+
(defn-io do-stuff [things]
51+
...do the same stuff...)"
52+
[name doc args & body]
53+
(let [[doc args body]
54+
(if (vector? args)
55+
[doc args body]
56+
["" doc (cons args body)])]
57+
`(defn ~name ~doc ~args
58+
(with-monad io-m
59+
~@body))))

src/clojure/pure_io/monad.clj

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
(ns clojure.pure-io.monad
2-
(:require [clojure.algo.monads :refer (defmonad with-monad)]))
3-
2+
(:require [clojure.algo.monads :refer (with-monad defmonad)]))
3+
44
(defprotocol PerformIO (-perform-io [io]))
55

66
(deftype IOResult [v]
@@ -20,8 +20,3 @@
2020
(if (instance? IOResult m)
2121
(f (.v m))
2222
(IOBind. m f)))])
23-
24-
(defmacro defn-io [name args & body]
25-
`(defn ~name [~@args]
26-
(with-monad io-m
27-
~@body)))

src/examples/clojure/pure_io/guessing.clj

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
(ns examples.clojure.pure-io.guessing
22
(:require [clojure.algo.monads :as m]
33
[clojure.pure-io.core :as io]
4-
[clojure.pure-io.monad :refer (defn-io)]
54
[clojure.pure-io.impl :refer (read-line' println')]))
65

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

3736
(declare number-game)
3837

39-
(defn-io eval-feedback [message secret]
38+
(io/defn-io eval-feedback [message secret]
4039
(let [m-message (println' message)]
4140
(if (= success message)
4241
m-message
4342
(m-bind m-message
4443
(constantly (number-game secret))))))
4544

46-
(defn-io number-game [secret-number]
45+
(io/defn-io number-game [secret-number]
4746
(m/domonad
4847
[guess-str (prompt' "Guess a number between 1 and 100!")
4948
:let [guess-num (cast-int guess-str)
5049
feedback (eval-guess secret-number guess-num)]
5150
result (eval-feedback feedback secret-number)]
5251
result))
5352

54-
(defn-io -main [& args]
53+
(io/defn-io -main
54+
"Follow the prompts to guess the proper number between on and one hundre"
55+
[& args]
5556
(io/perform-io!
5657
(let [m-secret-number (rand-int' 1 101)]
5758
(m-bind m-secret-number number-game))))
58-
59-
Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
(ns examples.clojure.pure-io.sorting
22
(:require [clojure.algo.monads :as m]
33
[clojure.pure-io.core :as io]
4-
[clojure.pure-io.monad :refer (defn-io)]
54
[clojure.pure-io.impl :refer (read-all' println')]
6-
[clojure.string :refer (split)]))
5+
[clojure.string :refer (split join)]))
76

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

10-
(def println-all' (partial apply println'))
9+
(def sort-lines
10+
(comp (partial join \newline)
11+
sort
12+
split-newline))
1113

12-
(defn-io -main [& args]
14+
(io/defn-io -main
15+
"Reads all lines from stdin, and prints them back in alphabetical order.
16+
Works best piping a file in, try sample.txt"
17+
[& _]
1318
(io/perform-io!
14-
(m-bind read-all'
15-
(comp println-all'
16-
sort
17-
split-newline))))
19+
(m-bind read-all' (comp println' sort-lines))))

test/clojure/pure_io/test/core.clj

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
(m-bind read-line' println')))
1111

1212
(defn bad-print [& args]
13-
(apply println args)
14-
(apply println' args))
13+
(println args)
14+
(println' args))
1515

1616
(defn- bad-read []
1717
(read-line)
@@ -23,19 +23,20 @@
2323
(with-out-str ~@body)))
2424

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

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

33-
(testing "Impure input throws exception"
34+
(testing "Impure output throws exception"
3435
(is (thrown-with-msg?
3536
Exception #"Impure IO!"
3637
(perform-io! (m-bind echo bad-print)))))
3738

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

0 commit comments

Comments
 (0)