rubactive 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: