cond 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/README ADDED
@@ -0,0 +1,334 @@
1
+
2
+ = Cond
3
+
4
+ == Description
5
+
6
+ Resolve errors without unwinding the stack.
7
+
8
+ == Synopsis
9
+
10
+ require 'cond'
11
+ include Cond
12
+
13
+ def divide(x, y)
14
+ restartable do
15
+ restart :return_this_instead do |value|
16
+ return value
17
+ end
18
+ raise ZeroDivisionError if y == 0
19
+ x/y
20
+ end
21
+ end
22
+
23
+ handling do
24
+ handle ZeroDivisionError do
25
+ invoke_restart :return_this_instead, 42
26
+ end
27
+ puts divide(10, 2) # => 5
28
+ puts divide(18, 3) # => 6
29
+ puts divide(4, 0) # => 42
30
+ puts divide(7, 0) # => 42
31
+ end
32
+
33
+ == Install
34
+
35
+ % gem install cond
36
+ or
37
+ % ruby install.rb [--uninstall]
38
+
39
+ == Overview
40
+
41
+ Cond allows errors to be handled near the place where they occur,
42
+ before the stack unwinds. It offers several advantages over
43
+ exceptions while peacefully coexisting with the standard exception
44
+ behavior.
45
+
46
+ The system is divided into two parts: _restarts_ and _handlers_. When
47
+ +raise+ is called and there is a matching handler for the error, the
48
+ normal mechanism of unwinding the stack is suspended while the handler
49
+ is called instead. At this time, the handler may invoke one of the
50
+ available restarts.
51
+
52
+ (1) program start (stack begin) --> +
53
+ |
54
+ |
55
+ |
56
+ |<-- handler_a
57
+ |
58
+ (2) handlers are set up -------> |<-- handler_b
59
+ |
60
+ |<-- handler_c -----+
61
+ | . |
62
+ | /|\ |
63
+ | | |
64
+ | | | (5) handler
65
+ | | | calls
66
+ | | | restart
67
+ | | |
68
+ +--------->------------- | ------+ |
69
+ | | (4) exception |
70
+ | | sent to |
71
+ | | handler |
72
+ | | |
73
+ | | |
74
+ | |<-- restart_x |
75
+ ^ | |
76
+ |<-- restart_y <----+
77
+ (3) raise called here here ------> |
78
+ +<-- restart_z
79
+
80
+ A handler may find a way to negate the problem and, by invoking a
81
+ restart, allow execution to continue from a place proximal to where
82
+ +raise+ was called. Or a handler may choose to allow the exception to
83
+ propagate in the usual unwinding fashion, as if the handler was never
84
+ called.
85
+
86
+ Cond is 100% compatible with the built-in exception-handling system.
87
+ We may imagine that Ruby had this handler/restart functionality from
88
+ very the beginning, but everyone had forgotten to write restarts. And
89
+ since no restarts were available, no handlers were written.
90
+
91
+ == Background
92
+
93
+ Cond is stolen from the Common Lisp condition system.
94
+
95
+ Peter Seibel discusses the advantages of handlers and restarts in the
96
+ following video. I have fast-forwarded to the most relevant part,
97
+ though the whole talk is worthwhile.
98
+
99
+ http://video.google.com/videoplay?docid=448441135356213813#46m07s
100
+
101
+ The example he shows is taken from his book on Lisp,
102
+
103
+ http://www.gigamonkeys.com/book/beyond-exception-handling-conditions-and-restarts.html
104
+
105
+ See readmes/seibel_pcl.rb for a Ruby translation.
106
+
107
+ == Synopsis 2.0
108
+
109
+ x, y = 7, 0
110
+
111
+ handling do
112
+ handle ZeroDivisionError do |exception|
113
+ invoke_restart :return_this_instead, 42
114
+ end
115
+
116
+ result = restartable do
117
+ restart :return_this_instead do |value|
118
+ leave value
119
+ end
120
+
121
+ raise ZeroDivisionError if y == 0
122
+ x/y
123
+ end
124
+
125
+ puts result # => 42
126
+ end
127
+
128
+ +leave+ acts like +return+ for the current +handling+ or +restartable+
129
+ block. Its counterpart is +again+, which acts like +redo+ for the
130
+ current +handling+ or +restartable+ block. These blocks may be nested
131
+ arbitrarily.
132
+
133
+ +leave+ and +again+ are for convenience only. They remove the need
134
+ for repetitive catch blocks and prevent symbol collisions for nested
135
+ catch labels.
136
+
137
+ A default handler is provided which runs a simple choose-a-restart
138
+ input loop when +raise+ is called, as the next example demonstrates.
139
+
140
+ == Restart Example
141
+
142
+ require 'pp'
143
+ require 'cond'
144
+ include Cond
145
+
146
+ class RestartableFetchError < RuntimeError
147
+ end
148
+
149
+ def read_new_value(what)
150
+ print("Enter a new #{what}: ")
151
+ eval($stdin.readline.chomp)
152
+ end
153
+
154
+ def restartable_fetch(hash, key, default = nil)
155
+ restartable do
156
+ restart :continue, "Return not having found the value." do
157
+ return default
158
+ end
159
+ restart :try_again, "Try getting the key from the hash again." do
160
+ again
161
+ end
162
+ restart :use_new_key, "Use a new key." do
163
+ key = read_new_value("key")
164
+ again
165
+ end
166
+ restart :use_new_hash, "Use a new hash." do
167
+ hash = read_new_value("hash")
168
+ again
169
+ end
170
+ hash.fetch(key) {
171
+ raise RestartableFetchError,
172
+ "Error getting #{key.inspect} from:\n#{hash.pretty_inspect}"
173
+ }
174
+ end
175
+ end
176
+
177
+ fruits_and_vegetables = Hash[*%w[
178
+ apple fruit
179
+ orange fruit
180
+ lettuce vegetable
181
+ tomato depends_on_who_you_ask
182
+ ]]
183
+
184
+ Cond.with_default_handlers {
185
+ puts("value: " + restartable_fetch(fruits_and_vegetables, "mango").inspect)
186
+ }
187
+
188
+ Run:
189
+
190
+ % ruby readmes/restarts.rb
191
+ readmes/restarts.rb:49:in `<main>'
192
+ Error getting "mango" from:
193
+ {"apple"=>"fruit",
194
+ "orange"=>"fruit",
195
+ "lettuce"=>"vegetable",
196
+ "tomato"=>"depends_on_who_you_ask"}
197
+
198
+ 0: Return not having found the value. (:continue)
199
+ 1: Try getting the key from the hash again. (:try_again)
200
+ 2: Use a new hash. (:use_new_hash)
201
+ 3: Use a new key. (:use_new_key)
202
+ Choose number: 3
203
+ Enter a new key: "apple"
204
+ value: "fruit"
205
+
206
+ % ruby readmes/restarts.rb
207
+ readmes/restarts.rb:49:in `<main>'
208
+ Error getting "mango" from:
209
+ {"apple"=>"fruit",
210
+ "orange"=>"fruit",
211
+ "lettuce"=>"vegetable",
212
+ "tomato"=>"depends_on_who_you_ask"}
213
+
214
+ 0: Return not having found the value. (:continue)
215
+ 1: Try getting the key from the hash again. (:try_again)
216
+ 2: Use a new hash. (:use_new_hash)
217
+ 3: Use a new key. (:use_new_key)
218
+ Choose number: 2
219
+ Enter a new hash: { "mango" => "mangoish fruit" }
220
+ value: "mangoish fruit"
221
+
222
+ Translated to Ruby from http://c2.com/cgi/wiki?LispRestartExample
223
+
224
+ == Technical Notes
225
+
226
+ Cond has been tested on MRI 1.8.6, 1.8.7, 1.9, and the latest jruby.
227
+
228
+ Each thread keeps its own list of handlers, restarts, and other data.
229
+ All operations are fully thread-safe.
230
+
231
+ It is not required to <tt>include Cond</tt>. The includable methods
232
+ of +Cond+ are <tt>module_function</tt>s and are thus callable via e.g.
233
+ <tt>Cond.handling</tt>.
234
+
235
+ +Cond+ nests private modules and classes inside Cond::CondPrivate in
236
+ order to improve the hygiene of <tt>include Cond</tt> and encourage
237
+ its use.
238
+
239
+ Except for the redefinition +raise+, Cond does not silently modify any
240
+ of the standard classes
241
+
242
+ The essential implementation is small and simple: it consists of two
243
+ per-thread stacks of hashes (handlers and restarts) with merge-push
244
+ and pop operations. The syntax shown in the above examples is a thin
245
+ layer concealing the underlying hashes. It is equivalent to the
246
+ following raw form. You are free to use either form according to
247
+ preference or circumstance.
248
+
249
+ === Raw Form
250
+
251
+ require 'cond'
252
+
253
+ def divide(x, y)
254
+ restarts = {
255
+ :return_this_instead => lambda { |value|
256
+ throw :leave, value
257
+ }
258
+ }
259
+ catch :leave do
260
+ Cond.with_restarts restarts do
261
+ raise ZeroDivisionError if y == 0
262
+ x/y
263
+ end
264
+ end
265
+ end
266
+
267
+ handlers = {
268
+ ZeroDivisionError => lambda { |exception|
269
+ Cond.invoke_restart :return_this_instead, 42
270
+ }
271
+ }
272
+ Cond.with_handlers handlers do
273
+ puts divide(10, 2) # => 5
274
+ puts divide(18, 3) # => 6
275
+ puts divide(4, 0) # => 42
276
+ puts divide(7, 0) # => 42
277
+ end
278
+
279
+ === Limitations
280
+
281
+ There must be a call to +raise+ inside Ruby code (as opposed to C
282
+ code) for a handler to be invoked.
283
+
284
+ The above synopsis gives an example: Why is there a check for division
285
+ by zero when +ZeroDivisionError+ would be raised anyway? Because
286
+ <tt>Fixnum#/</tt> is written in C.
287
+
288
+ It is still possible for handlers to intercept these raises, but it
289
+ requires redefining a wrapped version of the method in question:
290
+
291
+ Cond.wrap_instance_method(Fixnum, :/)
292
+
293
+ Once this has been called, the line
294
+
295
+ raise ZeroDivisionError if y == 0
296
+
297
+ is unnecessary.
298
+
299
+ It is possible remove this limitation by modifying the Ruby
300
+ interpreter to call Kernel#raise dynamically.
301
+
302
+ == Links
303
+
304
+ * Documentation: http://cond.rubyforge.org
305
+ * Rubyforge home: http://rubyforge.org/projects/cond/
306
+ * Download: http://rubyforge.org/frs/?group_id=7916
307
+ * Repository: http://github.com/quix/cond/tree/master
308
+
309
+ == Author
310
+
311
+ * James M. Lawrence < quixoticsycophant@gmail.com >
312
+
313
+ == License
314
+
315
+ Copyright (c) 2009 James M. Lawrence. All rights reserved.
316
+
317
+ Permission is hereby granted, free of charge, to any person obtaining
318
+ a copy of this software and associated documentation files (the
319
+ 'Software'), to deal in the Software without restriction, including
320
+ without limitation the rights to use, copy, modify, merge, publish,
321
+ distribute, sublicense, and/or sell copies of the Software, and to
322
+ permit persons to whom the Software is furnished to do so, subject to
323
+ the following conditions:
324
+
325
+ The above copyright notice and this permission notice shall be
326
+ included in all copies or substantial portions of the Software.
327
+
328
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
329
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
330
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
331
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
332
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
333
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
334
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,195 @@
1
+
2
+ require 'rake'
3
+ require 'spec/rake/spectask'
4
+ require 'rake/gempackagetask'
5
+ require 'rake/contrib/rubyforgepublisher'
6
+ require 'rdoc/rdoc'
7
+
8
+ require 'fileutils'
9
+ include FileUtils
10
+
11
+ README = "README"
12
+ PROJECT_NAME = "cond"
13
+ GEMSPEC = eval(File.read("#{PROJECT_NAME}.gemspec"))
14
+ raise unless GEMSPEC.name == PROJECT_NAME
15
+ DOC_DIR = "html"
16
+
17
+ SPEC_FILES = Dir['spec/*_spec.rb'] + Dir['examples/*_example.rb']
18
+ SPEC_OUTPUT = "spec_output.html"
19
+
20
+ ######################################################################
21
+ # default
22
+
23
+ task :default => :spec
24
+
25
+ ######################################################################
26
+ # spec
27
+
28
+ Spec::Rake::SpecTask.new('spec') do |t|
29
+ t.spec_files = SPEC_FILES
30
+ end
31
+
32
+ Spec::Rake::SpecTask.new('text_spec') do |t|
33
+ t.spec_files = SPEC_FILES
34
+ t.spec_opts = ['-fs']
35
+ end
36
+
37
+ Spec::Rake::SpecTask.new('full_spec') do |t|
38
+ t.spec_files = SPEC_FILES
39
+ t.rcov = true
40
+ exclude_dirs = %w[readmes support examples spec]
41
+ t.rcov_opts = exclude_dirs.inject(Array.new) { |acc, dir|
42
+ acc + ["--exclude", dir]
43
+ }
44
+ t.spec_opts = ["-fh:#{SPEC_OUTPUT}"]
45
+ end
46
+
47
+ task :show_full_spec => :full_spec do
48
+ args = SPEC_OUTPUT, "coverage/index.html"
49
+ open_browser(*args)
50
+ end
51
+
52
+ ######################################################################
53
+ # readme
54
+
55
+ task :readme do
56
+ readme = File.read(README)
57
+ restarts = File.read("readmes/restarts.rb")
58
+ run_re = %r!\A\# !
59
+ update = readme.sub(%r!(= Restart Example\n)(.*?)(?=^Run)!m) {
60
+ $1 + "\n" +
61
+ restarts[%r!^(require.*?)(?=^\#)!m].
62
+ gsub(%r!^!m, " ")
63
+ }.sub(%r!^(Run:\n)(.*?)(?=^\S)!m) {
64
+ $1 + "\n" +
65
+ restarts.lines.grep(run_re).map { |t| t.sub(run_re, " ") }.join + "\n"
66
+ }
67
+ File.open(README, "w") { |f| f.print update }
68
+ end
69
+
70
+ ######################################################################
71
+ # clean
72
+
73
+ task :clean => [:clobber, :clean_doc] do
74
+ end
75
+
76
+ task :clean_doc do
77
+ rm_rf(DOC_DIR)
78
+ rm_f(SPEC_OUTPUT)
79
+ end
80
+
81
+ ######################################################################
82
+ # package
83
+
84
+ task :package => :clean
85
+
86
+ Rake::GemPackageTask.new(GEMSPEC) { |t|
87
+ t.need_tar = true
88
+ }
89
+
90
+ ######################################################################
91
+ # doc
92
+
93
+ task :doc => :clean_doc do
94
+ files = %W[#{README} lib/cond.rb]
95
+
96
+ options = [
97
+ "-o", DOC_DIR,
98
+ "--title", "#{GEMSPEC.name}: #{GEMSPEC.summary}",
99
+ "--main", README
100
+ ]
101
+
102
+ RDoc::RDoc.new.document(files + options)
103
+ end
104
+
105
+ task :rdoc => :doc
106
+
107
+ task :show_doc => :doc do
108
+ open_browser("#{DOC_DIR}/index.html")
109
+ end
110
+
111
+ ######################################################################
112
+ # misc
113
+
114
+ def open_browser(*files)
115
+ if Config::CONFIG["host"] =~ %r!darwin!
116
+ sh("open", "/Applications/Firefox.app", *files)
117
+ else
118
+ sh("firefox", *files)
119
+ end
120
+ end
121
+
122
+ ######################################################################
123
+ # git
124
+
125
+ def git(*args)
126
+ sh("git", *args)
127
+ end
128
+
129
+ ######################################################################
130
+ # publisher
131
+
132
+ task :publish => :doc do
133
+ Rake::RubyForgePublisher.new(GEMSPEC.name, 'quix').upload
134
+ end
135
+
136
+ ######################################################################
137
+ # release
138
+
139
+ unless respond_to? :tap
140
+ module Kernel
141
+ def tap
142
+ yield self
143
+ self
144
+ end
145
+ end
146
+ end
147
+
148
+ task :prerelease => :clean do
149
+ rm_rf(DOC_DIR)
150
+ rm_rf("pkg")
151
+ unless `git status` =~ %r!nothing to commit \(working directory clean\)!
152
+ raise "Directory not clean"
153
+ end
154
+ unless `ping github.com 2 2` =~ %r!0% packet loss!i
155
+ raise "No ping for github.com"
156
+ end
157
+ end
158
+
159
+ def rubyforge(command, file)
160
+ sh(
161
+ "rubyforge",
162
+ command,
163
+ GEMSPEC.rubyforge_project,
164
+ GEMSPEC.rubyforge_project,
165
+ GEMSPEC.version.to_s,
166
+ file
167
+ )
168
+ end
169
+
170
+ task :finish_release do
171
+ gem, tgz = %w(gem tgz).map { |ext|
172
+ "pkg/#{GEMSPEC.name}-#{GEMSPEC.version}.#{ext}"
173
+ }
174
+ gem_md5, tgz_md5 = [gem, tgz].map { |file|
175
+ "#{file}.md5".tap { |md5|
176
+ sh("md5sum #{file} > #{md5}")
177
+ }
178
+ }
179
+
180
+ rubyforge("add_release", gem)
181
+ rubyforge("add_file", gem_md5)
182
+ rubyforge("add_file", tgz)
183
+ rubyforge("add_file", tgz_md5)
184
+
185
+ git("tag", "#{GEMSPEC.name}-" + GEMSPEC.version.to_s)
186
+ git(*%w(push --tags origin master))
187
+ end
188
+
189
+ task :release =>
190
+ [
191
+ :prerelease,
192
+ :package,
193
+ :publish,
194
+ :finish_release,
195
+ ]