cond 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+ ]