rubactive 0.0.1

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.
@@ -0,0 +1,8 @@
1
+ *.gem
2
+ .bundle
3
+ .yardoc
4
+ coverage
5
+ doc
6
+ Gemfile.lock
7
+ rdoc
8
+ pkg/*
data/.rvmrc ADDED
@@ -0,0 +1,2 @@
1
+ rvm_install_on_use_flag=1
2
+ rvm --create use ruby-1.9.2
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in rubactive.gemspec
4
+ gemspec
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2012 Brian Marick
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,283 @@
1
+ = Rubactive
2
+
3
+ I had a hard time figuring out what "functional reactive
4
+ programming" and "reactive programming" meant from
5
+ documentation on the web. Implementing it helped. This is
6
+ the implementation. You can tell me if I'm still confused.
7
+
8
+ Note: a big part of my confusion was because, I believe, the
9
+ terminology used obscures more than it reveals, so I tried
10
+ to pick better names. Again, you'll be the judge of whether
11
+ I succeeded.
12
+
13
+ The implementation I chose is most closely modeled after
14
+ Flapjax (http://www.flapjax-lang.org/). Their tutorial is
15
+ pretty good, though it uses the conventional names and is
16
+ occasionally obfuscated by having flapjax code mixed up with
17
+ HTML. Still: good job, guys!
18
+
19
+ Note: my implementation is extremely naive (for example, it
20
+ doesn't even try to deal with "glitching"), and it's missing
21
+ some of the nice utility functions Flapjax has.
22
+
23
+ = Rubactive
24
+
25
+ To use Rubactive, you'll need Ruby 1.9. (I use 1.9.2.) Do
26
+ this in irb:
27
+
28
+ require 'rubactive'
29
+ include Rubactive
30
+
31
+ The idea of reactive programming is that you have values
32
+ that change as a reaction to changes in other values. There
33
+ are two ways to look at such values:
34
+
35
+ [time-varying values]
36
+ There's a single value that changes over time. It might
37
+ be 5 at one moment and 6 at another. That might happen
38
+ because you explicitly changed it, but it might also
39
+ change because *some* *other* time-varying value changed
40
+ and this one reacted to that.
41
+
42
+ In Rubactive, such values are of class Rubactive::TimeVaryingValue.
43
+
44
+ [streams of values]
45
+ Instead of one value that changes, it can be convenient
46
+ to think of a stream of distinct values, arriving one at
47
+ a time. A stream might change because some code added a
48
+ value to it, or because it reacted to a new value
49
+ appearing on another stream.
50
+
51
+ In Rubactive, such streams are of class
52
+ Rubactive::DiscreteValueStream.
53
+
54
+ I used terminology like "a way to look at" and "convenient
55
+ to think of" because there's no huge difference between the
56
+ two classes. They are both thin wrappers (that mainly
57
+ provide different terminology) over a base ReactiveNode
58
+ class (which I haven't bothered to document).
59
+
60
+ == TimeVaryingValues
61
+
62
+ The simple way to create a time-varying value is to give it
63
+ a starting value:
64
+
65
+ origin = TimeVaryingValue.starting_with(0)
66
+
67
+ The current value of +origin+ can be found like this:
68
+
69
+ origin.current #=> 0
70
+
71
+ (Note: it's sort of lame that we refer to +origin+ as a
72
+ value but have to use +current+ to get the... value... of
73
+ the... value. Some reactive frameworks work to hide the fact
74
+ that +origin+ isn't really a value, but rather an
75
+ object-containing-a-value. I don't do that.)
76
+
77
+ We can also create another time-varying value that will
78
+ always be the same as +origin+:
79
+
80
+ exactly = TimeVaryingValue.follows(origin)
81
+ exactly.current #=> 0
82
+
83
+ Here's a way to see that +exactly+ really does follow
84
+ +origin+:
85
+
86
+ origin.change_to("dawn!")
87
+ exactly.current #=> "dawn!"
88
+
89
+ That's not wildly exciting, so let's have one time-varying
90
+ value be a function of another:
91
+
92
+ upper = TimeVaryingValue.follows(origin) { | o | o.upcase }
93
+ upper.current #=> "DAWN!"
94
+ origin.change_to("dawn, paul, and sophie")
95
+ upper.current #=> "DAWN, PAUL, AND SOPHIE"
96
+
97
+ (As noted before, it'd be better if time-varying values
98
+ looked like integers, or strings, or whatever, instead of
99
+ objects containing integers, or strings, or whatever. As a
100
+ gesture toward that, I made it so +method_missing+
101
+ constructed new time-varying-values:
102
+
103
+ coolness = origin.upcase
104
+ coolness.current # => "DAWN, PAUL, AND SOPHIE"
105
+ origin.change_to("your name here")
106
+ coolness.current # => "YOUR NAME HERE"
107
+
108
+ That really doesn't add anything to your understanding, but
109
+ what's the point of programming in Ruby if you can't show
110
+ off?)
111
+
112
+ There's no reason why time-varying values can't be dependent
113
+ on more than one "origin":
114
+
115
+ annoyance = TimeVaryingValue.starting_with(" [that's what she said]")
116
+ michael = coolness + annoyance
117
+ # above shorthand equivalent to:
118
+ # michael = TimeVaryingValue.follows(coolness, annoyance) do | c, a |
119
+ # c + a
120
+ # end
121
+ michael.current #=> "YOUR NAME HERE [that's what she said]"
122
+
123
+ annoyance.change_to(" in bed!")
124
+ michael.current #=> "YOUR NAME HERE in bed!"
125
+
126
+ == DiscreteValueStream
127
+
128
+ Now let's consider a stream of values, where a new value
129
+ might appear at any instant. (You can probably see how this
130
+ might be useful for modeling user input.) Here's how to
131
+ create a stream that doesn't depend on anything:
132
+
133
+ values = DiscreteValueStream.manual
134
+
135
+ You can put something onto a stream and look at it:
136
+
137
+ values.add_value(5)
138
+ values.most_recent_value #=> 5
139
+
140
+ As you might expect, you can have one stream follow another:
141
+
142
+ boring_values = DiscreteValueStream.manual
143
+ excited_values = DiscreteValueStream.follows(boring_values) do | b |
144
+ b.upcase + "!"
145
+ end
146
+
147
+ boring_values.add_value("party")
148
+ excited_values.most_recent_value #=> "PARTY!"
149
+
150
+ == The outside world
151
+
152
+ The "reactive world" is one in which values are tied
153
+ together with relationships created by +follows+. But that
154
+ reactive world is embedded within other code that's not
155
+ reactive. For example, it might be that a change to a
156
+ reactive value should make a user interface control update.
157
+
158
+ This can be done by handing a callback to the reactive
159
+ value. Here's how a new addition to a value stream can
160
+ affect the non-reactive world:
161
+
162
+ excited_values.on_addition do | most_recent |
163
+ puts "This new value has been added: #{most_recent}"
164
+ end
165
+
166
+ boring_values.add_value("vegetate")
167
+ # This new value has been added: VEGETATE!
168
+
169
+ The same can be done with time-varying-values, but the
170
+ method name is different (for clarity):
171
+
172
+ tvv = TimeVaryingValue.starting_with(8)
173
+ tvv.on_change do | current |
174
+ puts "New value: #{current.inspect}"
175
+ end
176
+
177
+ tvv.change_to("Veterinarians >> human medicine people")
178
+ New value: "Veterinarians >> human medicine people"
179
+
180
+ == An end-to-end-example
181
+
182
+ Consider a model-view-controller architecture that lets a
183
+ user control a particular hardware setting. The user
184
+ interface displays the current setting, and provides
185
+ controls that lets the user change it by some delta. In a
186
+ typical MVC implementation, the controller takes an active
187
+ role in shuttling events and values between layers of the
188
+ system. But that responsibility could be implemented
189
+ declaratively with reactive values.
190
+
191
+ Let's begin!
192
+
193
+ The current hardware setting is a time-varying value:
194
+
195
+ hardware_setting = TimeVaryingValue.starting_with(50)
196
+
197
+ The user's actions can be considered to be a stream of delta
198
+ values:
199
+
200
+ deltas = DiscreteValueStream.manual
201
+
202
+ That stream of deltas should be combined with the hardware
203
+ setting to produce a stream of desired settings:
204
+
205
+ user_changes = DiscreteValueStream.follows(deltas) do |delta|
206
+ delta + hardware_setting.current
207
+ end
208
+
209
+ (Alternately, we could be more terse:
210
+
211
+ user_changes = deltas + hardware_setting.current
212
+
213
+ ... but that would be showing off.)
214
+
215
+ Note: I'm not having the +user_changes+ follow the hardware
216
+ settings because independent changes to the hardware don't
217
+ count as *user* changes.
218
+
219
+ When a user asks for a change, the hardware should be told
220
+ to change. That steps out of the reactive framework, so it
221
+ requires a callback. I'm going to pretend that the callback
222
+ does various things, one of which is to change the
223
+ +hardware_setting+ value:
224
+
225
+ user_changes.on_addition do |value|
226
+ # The code talks to the real hardware and also sets the authoritative value:
227
+ hardware_setting.change_to(value)
228
+ end
229
+
230
+ At this point, we've propagated the user's desires
231
+ "downward" (toward the hardware), but we also have to
232
+ propagate the truth about the hardware upward (toward the
233
+ user interface). We could have the user interface directly
234
+ reflect the low-level +hardware_setting+ value, but lets
235
+ decouple things a bit by having the displayed value +follow+
236
+ the +hardware_setting+:
237
+
238
+ value_displayed = TimeVaryingValue.follows(hardware_setting)
239
+
240
+ (There'd presumably be some sort of +on_change+ callback to
241
+ communicate changes to the time-varying value to the
242
+ user-interface control.)
243
+
244
+ So, now that we've done this wiring, how does it work?
245
+
246
+ The hardware setting starts at 50, because we told it that's
247
+ the default:
248
+
249
+ hardware_setting.current #=> 50
250
+
251
+ Because the displayed value follows the hardware setting, it
252
+ too is 50:
253
+
254
+ value_displayed.current #=> 50
255
+
256
+ Suppose the user clicks a button, enters a text value, or drags
257
+ a slider---whatever. That provokes code that adds a value to
258
+ the +deltas+ stream. Let's simulate that:
259
+
260
+ deltas.add_value(5)
261
+
262
+ Did that count as a new +user_change+? Yes:
263
+
264
+ user_changes.most_recent_value #=> 55
265
+
266
+ Did that (via the callback) change the value of the
267
+ hardware setting? Yes:
268
+
269
+ hardware_setting.current #=> 55
270
+
271
+ Was that value reflected up to the user interface? Yes:
272
+
273
+ value_displayed.current #=> 55
274
+
275
+ All this was done declaratively (with a sort of DSL), rather
276
+ than with writing controller methods. That's the promise of
277
+ reactive programming.
278
+
279
+ == Copyright
280
+
281
+ Copyright (c) 2012 Brian Marick. See LICENSE.txt for
282
+ further details.
283
+
@@ -0,0 +1,13 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ require 'rake/testtask'
4
+ Rake::TestTask.new(:test) do |test|
5
+ test.libs << 'lib' << 'test'
6
+ test.pattern = 'test/**/*test*.rb'
7
+ test.verbose = true
8
+ end
9
+
10
+ task :default => :test
11
+
12
+ task :rdoc
13
+ `rdoc README.rdoc lib`
@@ -0,0 +1,2 @@
1
+ require "rubactive/version"
2
+ require "rubactive/rubactive"
@@ -0,0 +1,254 @@
1
+ require 'forwardable'
2
+
3
+ # Rubactive provides two kinds of values-that-react-to-changes-in-other-values.
4
+ #
5
+ # Sometimes you want to think of the changing value as being something
6
+ # that "magically" changes over time (typically in reaction to changes
7
+ # in other values). That perspective is implemented by TimeVaryingValue.
8
+ #
9
+ # Sometimes you want to think of a stream of unchanging values that
10
+ # "magically" arrive (often in reaction to values appearing in other
11
+ # streams). That perspective is implemented by DiscreteValueStream.
12
+ #
13
+
14
+
15
+ module Rubactive
16
+ class ReactiveNode # :nodoc:
17
+ # This should probably be a delegate instead of superclass
18
+ # because the subclasses don't want all of the methods
19
+ # (but the tests do)
20
+ private_class_method :new
21
+
22
+ def self.follows(*earlier_nodes, &updater)
23
+ new(*earlier_nodes, &updater)
24
+ end
25
+
26
+ attr_reader :value
27
+
28
+ DEFAULT_VALUE = :no_value_at_all
29
+
30
+ def initialize(*earlier_nodes, &recalculator)
31
+ @value = DEFAULT_VALUE
32
+ @recalculator = recalculator || ->val {val}
33
+ @later_nodes = []
34
+ @earlier_nodes = earlier_nodes
35
+ @change_callback = ->ignored{}
36
+ tell_earlier_nodes_about_me(earlier_nodes)
37
+ end
38
+
39
+ def tell_earlier_nodes_about_me(earlier_nodes)
40
+ earlier_nodes.each do |e|
41
+ e.this_node_is_later_than_you(self) if e.is_a?(ReactiveNode)
42
+ end
43
+ end
44
+
45
+ def this_node_is_later_than_you(this_node)
46
+ @later_nodes << this_node
47
+ end
48
+
49
+ def recalculate
50
+ propagate(@recalculator.call(*just_values(@earlier_nodes)))
51
+ end
52
+
53
+ def value=(new_value)
54
+ propagate(new_value)
55
+ end
56
+
57
+ # When the value of this variable changes, call the block argument.
58
+ def on_change(&block)
59
+ @change_callback = block
60
+ end
61
+
62
+ def propagate(value)
63
+ @value = value
64
+ @change_callback.(value)
65
+ @later_nodes.each do |node|
66
+ node.recalculate
67
+ end
68
+ end
69
+
70
+ def method_missing(message, *args)
71
+ recalculator = lambda do |*just_values|
72
+ receiver = just_values.shift
73
+ receiver.send(message, *just_values)
74
+ end
75
+ self.class.follows(self, *args, &recalculator)
76
+ end
77
+
78
+ def just_values(args)
79
+ args.collect do |arg|
80
+ if arg.is_a?(ReactiveNode)
81
+ arg.value
82
+ else
83
+ arg
84
+ end
85
+ end
86
+ end
87
+
88
+ # test_support
89
+
90
+ def self.blank
91
+ follows() {}
92
+ end
93
+
94
+ end
95
+
96
+ # A TimeVaryingValue represents a value that might "mysteriously"
97
+ # change each time you look at it.
98
+ #
99
+ # The current value can change in three ways:
100
+ #
101
+ # * Explicitly, via change_to. That's no different than setting an attribute of any
102
+ # sort of object.
103
+ #
104
+ # * It might have been created to "follow" other time-varying values. In that case,
105
+ # it will react to any of their changes by recalculating itself (in a way
106
+ # defined when it was created.)
107
+ #
108
+ # * It might have been created to follow a DiscreteValueStream. In that case, any
109
+ # value added to the stream becomes the time-varying value's current value.
110
+ #
111
+ # There is a different constructor for each of the above cases. In addition, variables
112
+ # can be implicity created by sending unrecognized messages to other time-varying values.
113
+ #
114
+ # origin = TimeVaryingValue.starting_with(5)
115
+ # follower = origin + 1
116
+ # follower.current #=> 6
117
+ #
118
+ # origin.change_to(700)
119
+ # follower.current #=> 701
120
+ class TimeVaryingValue < ReactiveNode
121
+ # Create a value that changes as the values it depends upon change.
122
+ #
123
+ # When any followed variables change, the block is called with their current
124
+ # values. The result becomes the current value for this TimeVaryingValue.
125
+ #
126
+ # Example:
127
+ # verb = TimeVaryingValue.starting_with("vote")
128
+ # adverb = TimeVaryingValue.starting_with("early")
129
+ # tracker = TimeVaryingValue.follows(verb, adverb) { | v, a | "#{v} #{a}!" }
130
+ # tracker.current #=> "vote early!"
131
+ #
132
+ # adverb.change_to("often")
133
+ # tracker.current #=> "vote often!"
134
+ #
135
+ # If no block is given, this value should be following only one other.
136
+ # It adopts that other value whenever it changes.
137
+ #--
138
+ # Defined here so that I can write special-purpose documentation.
139
+ def self.follows(*values, &block)
140
+ super
141
+ end
142
+
143
+ # Create a new TimeVaryingValue.
144
+ #
145
+ # Since the returned instance follows nothing, only change_to can
146
+ # be used to change its value.
147
+ def self.starting_with(initial_value)
148
+ follows { initial_value }
149
+ end
150
+
151
+ # Create a time-varying value that follows the latest value in a stream.
152
+ #
153
+ # The first argument must be a DiscreteValueStream. Whenever that stream
154
+ # has a new value added, the time-varying value changes to match.
155
+ #
156
+ # The time-varying value always starts with the given initial_value, even
157
+ # if stream has previously had some values added to it.
158
+ #--
159
+ # Flapjax startsWith
160
+ def self.tracks_stream(value_stream, initial_value)
161
+ retval = follows(value_stream)
162
+ retval.change_to(initial_value)
163
+ retval
164
+ end
165
+
166
+ # Retrieve the current value.
167
+ #
168
+ # Earlier values are inaccessible.
169
+ def current; value; end
170
+
171
+ # Change the current value.
172
+ def change_to(new_value); self.value=new_value; end
173
+
174
+ def initialize(*earlier_nodes, &recalculator) # :nodoc:
175
+ super
176
+ recalculate
177
+ end
178
+
179
+ end
180
+
181
+ # A DiscreteValueStream represents a stream of values. When a value is added to the stream,
182
+ # other DiscreteValueStreams may react if they follow this stream.
183
+ #
184
+ # Streams are explicitly created with DiscreteValueStream.manual or
185
+ # DiscreteValueStream.follows. In the first case, values are added
186
+ # only with add_value. In the second, values can also be added in
187
+ # reaction to the streams being followed.
188
+ #
189
+ # Streams can also be implicity created by sending other streams unrecognized messages.
190
+ #
191
+ # origin = DiscreteValueStream.manual
192
+ # follower = origin + 1
193
+ #
194
+ # The previous definition of the follower stream is equivalent to this:
195
+ #
196
+ # follower = DiscreteValueStream.follows(origin) { | o | o + 1 }
197
+ class DiscreteValueStream < ReactiveNode
198
+ # Create a stream that reacts to one or more other streams.
199
+ #
200
+ # The addition of values to any of the streams will cause the block
201
+ # to be called with their most recent values. The result is added to
202
+ # this stream (as with add_value).
203
+ #
204
+ # Example:
205
+ # origin = DiscreteValueStream.manual
206
+ # follower = DiscreteValueStream.follows(origin) { | o | o+1 }
207
+ # origin.add_value(5)
208
+ # follower.most_recent_value #=> 6
209
+ #
210
+ # If no block is given, this stream should be following only one other stream.
211
+ # The value just added to that stream is also added to this one.
212
+ #--
213
+ # Defined here so that I can write special-purpose documentation.
214
+ def self.follows(*streams, &block)
215
+ super
216
+ end
217
+
218
+ # Create an empty value stream
219
+ #
220
+ # Use add_value to insert values into the stream.
221
+ def self.manual
222
+ follows {
223
+ raise "Incorrect use of recalculation in a manual event stream"
224
+ }
225
+ end
226
+
227
+ # Run a callback when a new value is added.
228
+ #
229
+ # This is an interface to the non-reactive world. When a new value is
230
+ # added (whether with add_value or in reaction to a followed stream),
231
+ # the callback is called and given that value. The callback will typically
232
+ # do something with the value, like add it to a GUI.
233
+ def on_addition(&callback); on_change(&callback); end
234
+
235
+
236
+ # Retrieve last value added to the stream
237
+ #
238
+ # Earlier values are inaccessible.
239
+ #
240
+ # It is an error to ask for the value of an #empty? stream.
241
+ def most_recent_value; @value; end
242
+
243
+ # Place a new value on the stream
244
+ def add_value(new_value) # this?
245
+ self.value = new_value
246
+ end
247
+
248
+ # True iff no value has ever been added to the stream.
249
+ def empty?
250
+ most_recent_value == DEFAULT_VALUE
251
+ end
252
+ end
253
+ end
254
+
@@ -0,0 +1,3 @@
1
+ module Rubactive
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,29 @@
1
+
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "rubactive/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "rubactive"
7
+ s.homepage = "http://github.com/marick/rubactive"
8
+ s.license = "MIT"
9
+ s.summary = %Q{A basic and perhaps misinformed library for learning about reactive programming}
10
+ s.description = %Q{A basic and perhaps misinformed library for learning about reactive programming}
11
+ s.email = "marick@exampler.com"
12
+ s.authors = ["Brian Marick"]
13
+ s.required_ruby_version = '>= 1.9.2'
14
+ s.version = Rubactive::VERSION
15
+
16
+ s.rubyforge_project = "rubactive"
17
+
18
+ s.files = `git ls-files`.split("\n")
19
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
20
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
21
+ s.require_paths = ["lib"]
22
+
23
+ s.add_development_dependency "shoulda", ">= 0"
24
+ s.add_development_dependency "assert2"
25
+ s.add_development_dependency "rr"
26
+ s.add_development_dependency "bundler", "~> 1.0.0"
27
+ s.add_development_dependency "jeweler", "~> 1.6.4"
28
+ s.add_development_dependency "test-unit"
29
+ end
@@ -0,0 +1,53 @@
1
+ require 'rubactive'
2
+ require_relative 'testutil'
3
+
4
+ class RubactiveUseCaseTests < Test::Unit::TestCase
5
+ include Rubactive
6
+
7
+ MIN=0
8
+ MAX=100
9
+ DEFAULT=50
10
+
11
+ def setup
12
+ # What our program knows about the current setting.
13
+ @hardware_setting = TimeVaryingValue.starting_with(DEFAULT)
14
+
15
+ # User-initiated deltas to the hardware setting
16
+ @deltas = DiscreteValueStream.manual
17
+
18
+ # Convert a stream of deltas to a stream of actual values
19
+ @user_changes = DiscreteValueStream.follows(@deltas) do |delta|
20
+ delta + @hardware_setting.current
21
+ end
22
+
23
+ # Callback to code that manipulates hardware.
24
+ @user_changes.on_addition do |value|
25
+ # The code talks to the real hardware and also sets the authoritative value:
26
+ @hardware_setting.change_to(value)
27
+ end
28
+
29
+ #Pretend this is the value of a slider or something
30
+ @value_displayed = TimeVaryingValue.follows(@hardware_setting)
31
+ end
32
+
33
+ should "propagate user changes" do
34
+ @deltas.add_value(5)
35
+
36
+ assert_equal(55, @hardware_setting.current)
37
+ assert_equal(55, @value_displayed.current)
38
+ end
39
+
40
+ should "propagate and obey hardware changes" do
41
+ @hardware_setting.change_to(80)
42
+
43
+ assert_equal(80, @hardware_setting.current)
44
+ assert_equal(80, @value_displayed.current)
45
+
46
+ @deltas.add_value(5)
47
+
48
+ assert_equal(85, @hardware_setting.current)
49
+ assert_equal(85, @value_displayed.current)
50
+ end
51
+ end
52
+
53
+
@@ -0,0 +1,244 @@
1
+ require 'rubactive'
2
+ require_relative 'testutil'
3
+
4
+ class Fixnum
5
+ # A useful N-argument method for testing
6
+ def max3(other1, other2)
7
+ [self, other1, other2].max
8
+ end
9
+ end
10
+
11
+
12
+ class RubactiveTests < Test::Unit::TestCase
13
+ include Rubactive
14
+
15
+ context "time-varying values" do
16
+ should "hold values" do
17
+ v = TimeVaryingValue.starting_with(3)
18
+ assert_equal(3, v.current)
19
+ v.change_to(4)
20
+ assert_equal(4, v.current)
21
+ end
22
+
23
+ context "following" do
24
+
25
+ should "allow other time-varying values" do
26
+ origin = TimeVaryingValue.starting_with(3)
27
+ destination = TimeVaryingValue.follows(origin) { |o| o + 1}
28
+ assert_equal(4, destination.current)
29
+ origin.change_to(4)
30
+ assert_equal(5, destination.current)
31
+ end
32
+
33
+ should "allow chains" do
34
+ origin = TimeVaryingValue.starting_with(3)
35
+ middle = TimeVaryingValue.follows(origin) { |o| o + 1}
36
+ destination = TimeVaryingValue.follows(middle) { |o| o + 1}
37
+
38
+ assert_equal(4, middle.current)
39
+ assert_equal(5, destination.current)
40
+
41
+ origin.change_to(40)
42
+ assert_equal(41, middle.current)
43
+ assert_equal(42, destination.current)
44
+ end
45
+
46
+ should "allow discrete value streams" do
47
+ changes = DiscreteValueStream.manual
48
+ tracker = TimeVaryingValue.tracks_stream(changes, 88)
49
+ assert_equal(88, tracker.current)
50
+ changes.add_value(5)
51
+ assert_equal(5, tracker.current)
52
+ end
53
+ end
54
+
55
+ context "implicit creation" do
56
+
57
+ should "generates value calculation from method-missing" do
58
+ origin = TimeVaryingValue.starting_with(8)
59
+ destination = origin + 1
60
+ assert_equal(9, destination.current)
61
+ assert_equal(TimeVaryingValue, destination.class)
62
+
63
+ origin.change_to(33)
64
+ assert_equal(34, destination.current)
65
+ end
66
+
67
+ should "work with multi-argument methods" do
68
+ assert_equal(3, 1.max3(2, 3))
69
+
70
+ origin = TimeVaryingValue.starting_with(2)
71
+ other = origin * -1
72
+ final = origin.max3(8, other)
73
+ assert_equal(8, final.current)
74
+
75
+ origin.change_to(100)
76
+ assert_equal(100, final.current)
77
+
78
+ origin.change_to(-222)
79
+ assert_equal(222, final.current)
80
+ end
81
+ end
82
+ end
83
+
84
+ context "discrete value streams" do
85
+ should "remember their most recent value" do
86
+ s = DiscreteValueStream.manual
87
+ s.add_value(33)
88
+ assert_equal(33, s.most_recent_value)
89
+ end
90
+
91
+ should "start out with a null-like value" do
92
+ s = DiscreteValueStream.manual
93
+ assert_equal(:no_value_at_all, s.most_recent_value)
94
+ assert_true(true, s.empty?)
95
+ end
96
+
97
+ should "be able to create new event streams from old" do
98
+ stream = DiscreteValueStream.manual
99
+ transformed = DiscreteValueStream.follows(stream) { |s|
100
+ s + 1
101
+ }
102
+ stream.add_value(33)
103
+ assert_equal(34, transformed.most_recent_value)
104
+ end
105
+
106
+ should "be able to create streams implicitly" do
107
+ stream = DiscreteValueStream.manual
108
+ transformed = stream + 1
109
+ assert_equal(DiscreteValueStream, transformed.class)
110
+
111
+ stream.add_value(33)
112
+ assert_equal(34, transformed.most_recent_value)
113
+ end
114
+ end
115
+
116
+ ### Behavior common to both types of value-holders
117
+ ### Not a public API
118
+
119
+ context "Reactive Nodes" do
120
+ should "take a block that calculates their value" do
121
+ n = ReactiveNode.blank
122
+ n.value=5
123
+ assert_equal(5, n.value)
124
+ end
125
+
126
+ should "be able to depend on other nodes" do
127
+ before = ReactiveNode.blank
128
+ before.value = 5
129
+
130
+ after = ReactiveNode.follows(before) do |b|
131
+ 1 + b
132
+ end
133
+ after.recalculate
134
+ assert_equal(6, after.value)
135
+
136
+ before.value = 88
137
+ assert_equal(89, after.value)
138
+ end
139
+
140
+ should "be able to depend on a combination of nodes" do
141
+ a_node = ReactiveNode.blank
142
+ a_node.value = 1
143
+
144
+ b_node = ReactiveNode.blank
145
+ b_node.value = 10
146
+
147
+ captured = 100
148
+
149
+ combiner = ReactiveNode.follows(a_node, b_node) do |a,b|
150
+ a+b+captured
151
+ end
152
+
153
+ combiner.recalculate
154
+ assert_equal(111, combiner.value)
155
+
156
+ a_node.value = 20000
157
+ assert_equal(20110, combiner.value)
158
+
159
+ # unsurprisingly, changing the plain variable triggers no propagation
160
+ captured = -a_node.value
161
+ assert_equal(20110, combiner.value)
162
+
163
+ b_node.value = 88
164
+ assert_equal(88, combiner.value)
165
+ end
166
+
167
+ should "follow a variable if no block given" do
168
+ before = ReactiveNode.blank
169
+ after = ReactiveNode.follows(before)
170
+ before.value = 88
171
+ assert_equal(88, after.value)
172
+
173
+ end
174
+
175
+ should "be able to generate nodes implicitly" do
176
+ origin = ReactiveNode.blank
177
+ origin.value = 8
178
+ destination = origin + 1
179
+ destination.recalculate
180
+ assert_equal(9, destination.value)
181
+
182
+ origin.value=33
183
+ assert_equal(34, destination.value)
184
+
185
+ final_destination = destination + origin
186
+ final_destination.recalculate
187
+ assert_equal(67, final_destination.value)
188
+
189
+ origin.value=1
190
+ assert_equal(2, destination.value)
191
+ assert_equal(3, final_destination.value)
192
+ end
193
+
194
+ should "capture many types of value-containing-things" do
195
+ origin = ReactiveNode.blank
196
+ other = ReactiveNode.blank
197
+ other.value = 10
198
+
199
+ captured = 100
200
+ destination = origin + captured + other + 1
201
+
202
+ origin.value=1000
203
+ assert_equal(1111, destination.value)
204
+
205
+ captured = 999999 # This does not trigger update - no surprise
206
+ assert_equal(1111, destination.value)
207
+
208
+ # But it also has no effect on any future updates.
209
+ origin.value=2000
210
+ assert_equal(2111, destination.value)
211
+ end
212
+
213
+ context "callbacks to update the outside world" do
214
+
215
+ should "happen on recalculation" do
216
+ origin = ReactiveNode.blank
217
+ destination = ReactiveNode.follows(origin) {|o| o.to_s.upcase.to_sym}
218
+
219
+ triggered = false
220
+ destination.on_change do | value |
221
+ triggered = value
222
+ end
223
+
224
+ origin.value = :new_value
225
+
226
+ assert_equal(:NEW_VALUE, triggered)
227
+ end
228
+
229
+ should "happen on value-setting" do
230
+ n = ReactiveNode.blank
231
+
232
+ triggered = false
233
+ n.on_change do | value |
234
+ triggered = value
235
+ end
236
+
237
+ n.value = :new_value
238
+ assert_equal(:new_value, triggered)
239
+ end
240
+ end
241
+ end
242
+
243
+
244
+ end
@@ -0,0 +1,18 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ begin
4
+ Bundler.setup(:default, :development)
5
+ rescue Bundler::BundlerError => e
6
+ $stderr.puts e.message
7
+ $stderr.puts "Run `bundle install` to install missing gems"
8
+ exit e.status_code
9
+ end
10
+ require 'test/unit'
11
+ require 'shoulda'
12
+ require 'rr'
13
+
14
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
15
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
16
+
17
+ require_relative 'testutil/mock-talk'
18
+
@@ -0,0 +1,76 @@
1
+ class Test::Unit::TestCase
2
+ include RR::Adapters::TestUnit
3
+ end
4
+
5
+ # These methods are used to change the flow of control so that the
6
+ # test can state what is to happen before stating what the mock should
7
+ # receive.
8
+ class Test::Unit::TestCase
9
+ def during(&block)
10
+ # Use of global needed for current way of handling
11
+ # listeners:
12
+ # listeners_to(@sut).run_methods {...}
13
+ $what_runs_after_mock_setup = block
14
+ self
15
+ end
16
+
17
+ def behold!
18
+ yield
19
+ @result = $what_runs_after_mock_setup.call
20
+ end
21
+
22
+ def listeners_to(object)
23
+ klass = object.class.const_get("Listener")
24
+ any_old_listener = klass.new
25
+ object.add_listener(any_old_listener)
26
+
27
+ # mock(any_old_listener).increase_setting
28
+ mockable = mock(any_old_listener)
29
+
30
+ def mockable.are_sent(&block)
31
+ instance_eval(&block)
32
+ $what_runs_after_mock_setup.call
33
+ end
34
+ mockable
35
+ end
36
+
37
+ def mocked(n = 1)
38
+ if (n == 1)
39
+ one_mock
40
+ else
41
+ (0...n).collect do
42
+ one_mock
43
+ end
44
+ end
45
+ end
46
+
47
+ def one_mock
48
+ core_object = Object.new
49
+ mock_wrapper = mock(core_object)
50
+ core_object.define_singleton_method(:is_sent) { mock_wrapper }
51
+ core_object
52
+ end
53
+
54
+
55
+ def collaborators(*names)
56
+ ensure_strings(names).each do | name |
57
+ result = one_mock()
58
+ self.instance_variable_set(instance_name(name), result)
59
+ end
60
+ end
61
+ alias_method :collaborator, :collaborators
62
+
63
+
64
+ private
65
+
66
+ def ensure_strings(maybe_symbols)
67
+ maybe_symbols.collect(&:to_s)
68
+ end
69
+
70
+ def instance_name(raw_name)
71
+ "@" + raw_name.to_s
72
+ end
73
+
74
+
75
+
76
+ end
metadata ADDED
@@ -0,0 +1,130 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rubactive
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Brian Marick
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-03-01 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: shoulda
16
+ requirement: &2169222580 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: *2169222580
25
+ - !ruby/object:Gem::Dependency
26
+ name: assert2
27
+ requirement: &2169222140 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: *2169222140
36
+ - !ruby/object:Gem::Dependency
37
+ name: rr
38
+ requirement: &2169221560 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :development
45
+ prerelease: false
46
+ version_requirements: *2169221560
47
+ - !ruby/object:Gem::Dependency
48
+ name: bundler
49
+ requirement: &2169220900 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: 1.0.0
55
+ type: :development
56
+ prerelease: false
57
+ version_requirements: *2169220900
58
+ - !ruby/object:Gem::Dependency
59
+ name: jeweler
60
+ requirement: &2169220240 !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ~>
64
+ - !ruby/object:Gem::Version
65
+ version: 1.6.4
66
+ type: :development
67
+ prerelease: false
68
+ version_requirements: *2169220240
69
+ - !ruby/object:Gem::Dependency
70
+ name: test-unit
71
+ requirement: &2169219700 !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ! '>='
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: *2169219700
80
+ description: A basic and perhaps misinformed library for learning about reactive programming
81
+ email: marick@exampler.com
82
+ executables: []
83
+ extensions: []
84
+ extra_rdoc_files: []
85
+ files:
86
+ - .gitignore
87
+ - .rvmrc
88
+ - Gemfile
89
+ - LICENSE.txt
90
+ - README.rdoc
91
+ - Rakefile
92
+ - lib/rubactive.rb
93
+ - lib/rubactive/rubactive.rb
94
+ - lib/rubactive/version.rb
95
+ - rubactive.gemspec
96
+ - test/rubactive-end-to-end-ish-tests.rb
97
+ - test/rubactive-tests.rb
98
+ - test/testutil.rb
99
+ - test/testutil/mock-talk.rb
100
+ homepage: http://github.com/marick/rubactive
101
+ licenses:
102
+ - MIT
103
+ post_install_message:
104
+ rdoc_options: []
105
+ require_paths:
106
+ - lib
107
+ required_ruby_version: !ruby/object:Gem::Requirement
108
+ none: false
109
+ requirements:
110
+ - - ! '>='
111
+ - !ruby/object:Gem::Version
112
+ version: 1.9.2
113
+ required_rubygems_version: !ruby/object:Gem::Requirement
114
+ none: false
115
+ requirements:
116
+ - - ! '>='
117
+ - !ruby/object:Gem::Version
118
+ version: '0'
119
+ requirements: []
120
+ rubyforge_project: rubactive
121
+ rubygems_version: 1.8.10
122
+ signing_key:
123
+ specification_version: 3
124
+ summary: A basic and perhaps misinformed library for learning about reactive programming
125
+ test_files:
126
+ - test/rubactive-end-to-end-ish-tests.rb
127
+ - test/rubactive-tests.rb
128
+ - test/testutil.rb
129
+ - test/testutil/mock-talk.rb
130
+ has_rdoc: