aboutsummaryrefslogtreecommitdiff
path: root/src/clojure/contrib/monadic_io_streams.clj
blob: 28772283e868cb755bd4a4d0df2ac3e8f9dec93c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
;; Monadic I/O

;; by Konrad Hinsen
;; last updated June 24, 2009

;; Copyright (c) Konrad Hinsen, 2009. All rights reserved.  The use
;; and distribution terms for this software are covered by the Eclipse
;; Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php)
;; which can be found in the file epl-v10.html at the root of this
;; distribution.  By using this software in any fashion, you are
;; agreeing to be bound by the terms of this license.  You must not
;; remove this notice, or any other, from this software.

(ns
  #^{:author "Konrad Hinsen"
     :doc "Monadic I/O with Java input/output streams
           Defines monadic I/O statements to be used in a state monad
           with an input or output stream as the state. The macro
           monadic-io creates a stream, runs a monadic I/O statement
           on it, and closes the stream. This structure permits the
           definition of purely functional compound I/O statements
           which are applied to streams that can never escape from the
           monadic statement sequence."}
  clojure.contrib.monadic-io-streams
  (:refer-clojure :exclude (read-line print println flush))
  (:use [clojure.contrib.monads
	 :only (with-monad domonad state-m state-m-until)])
  (:use [clojure.contrib.generic.functor :only (fmap)])
  (:use [clojure.contrib.duck-streams :only (reader writer)]))

;
; Wrap the state into a closure to make sure that "evil" code
; can't obtain the stream using fetch-state and manipulate it.
;
(let [key (Object.)
      lock (fn [state] (fn [x] (if (identical? x key) state nil)))
      unlock (fn [state] (state key))]

  ;
  ; Basic stream I/O statements as provided by Java
  ;
  (defn read-char
    "Read a single character"
    []
    (fn [s] [(.read (unlock s)) s]))

  (defn read-line
    "Read a single line"
    []
    (fn [s] [(.readLine (unlock s)) s]))

  (defn skip-chars
    "Skip n characters"
    [n]
    (fn [s] [(.skip (unlock s) n) s]))

  (defn write
    "Write text (a string)"
    [#^String text]
    (fn [s] [(.write (unlock s) text) s]))

  (defn flush
    "Flush"
    []
    (fn [s] [(.flush (unlock s)) s]))

  (defn print
    "Print obj"
    [obj]
    (fn [s] [(.print (unlock s) obj) s]))

  (defn println
    "Print obj followed by a newline"
    ([]
     (fn [s] [(.println (unlock s)) s]))
    ([obj]
     (fn [s] [(.println (unlock s) obj) s])))

  ;
  ; Inject I/O streams into monadic I/O statements
  ;
  (defn with-reader
    "Create a reader from reader-spec, run the monadic I/O statement
     on it, and close the reader. reader-spec can be any object accepted
     by clojure.contrib.duck-streams/reader."
    [reader-spec statement]
    (with-open [r (reader reader-spec)]
      (first (statement (lock r)))))

  (defn with-writer
    "Create a writer from writer-spec, run the monadic I/O statement
     on it, and close the writer. writer-spec can be any object accepted
     by clojure.contrib.duck-streams/writer."
    [writer-spec statement]
    (with-open [w (writer writer-spec)]
      (first (statement (lock w)))))

  (defn with-io-streams
    "Open one or more streams as specified by io-spec, run a monadic
     I/O statement on them, and close the streams. io-spec is
     a binding-like vector in which each stream is specified by
     three element: a keyword by which the stream can be referred to,
     the stream mode (:read or :write), and a stream specification as
     accepted by clojure.contrib.duck-streams/reader (mode :read) or
     clojure.contrib.duck-streams/writer (mode :write). The statement
     is run on a state which is a map from keywords to corresponding
     streams. Single-stream monadic I/O statements must be wrapped
     with clojure.contrib.monads/with-state-field."
    [io-specs statement]
    (letfn [(run-io [io-specs state statement]
	      (if (zero? (count io-specs))
		(first (statement state))
		(let [[[key mode stream-spec] & r] io-specs
		      opener (cond (= mode :read) reader
				   (= mode :write) writer
				   :else (throw
					  (Exception.
					   "Mode must be :read or :write")))]
		  (with-open [stream (opener stream-spec)]
		    (run-io r (assoc state key (lock stream)) statement)))))]
      (run-io (partition 3 io-specs) {} statement))))

;
; Compound I/O statements
;
(with-monad state-m

  (defn- add-line
    "Read one line and add it to the end of the vector lines. Return
     [lines eof], where eof is an end-of-file flag. The input eof argument
     is not used."
    [[lines eof]]
    (domonad
      [line (read-line)]
      (if (nil? line)
        [lines true]
        [(conj lines line) false])))

  (defn read-lines
    "Read all lines and return them in a vector"
    []
    (domonad
      [[lines eof] (state-m-until second add-line [[] false])]
      lines)))