oscillo 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in oscillo.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Evan Tatarka
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,133 @@
1
+ # Oscillo
2
+
3
+ Based off of functional reactive programming, oscillo gives you a signal that
4
+ represents a value changing over time. You can manipulate signals and combine
5
+ them together in various ways.
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ gem 'oscillo'
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install oscillo
20
+
21
+ ## Usage
22
+
23
+ ### Creating and using signals
24
+
25
+ Creating a signal is as simple as
26
+
27
+ s = Oscillo::Signal.new
28
+
29
+ You modify the value of the signal with `s << :value` and you get the current
30
+ value of the signal with `s.value`, or `s.val` if you want to save a couple of
31
+ letters.
32
+
33
+ If you want to give a signal a starting value, you just pass it to the
34
+ constructor.
35
+
36
+ s = Oscillo::Signal.new(0)
37
+
38
+ ### Following other signals
39
+
40
+ If you pass another signal as an argument to `new`, the signal's value will
41
+ follow the other one.
42
+
43
+ a = Oscillo::Signal.new
44
+ b = Oscillo::Signal.new(a)
45
+
46
+ a << :value
47
+ b.val #=> :value
48
+
49
+ You can pass a block to `new` to modify how the new signal follows the old.
50
+
51
+ a = Oscillo::Signal.new(0)
52
+ b = Oscillo::Signal.new(a) { |v| v * 2 }
53
+
54
+ a << 3
55
+ b.val #=> 6
56
+
57
+ You can also follow multiple signals at once. The new signal will change if any
58
+ of the signal that you follow changes.
59
+
60
+ a = Oscillo::Signal.new(0)
61
+ b = Oscillo::Signal.new(0)
62
+ c = Oscillo::Signal.new(a, b) { |v1, v2| v1 + v2 }
63
+
64
+ a << 2
65
+ c.val #=> 2
66
+ b << 3
67
+ c.val #=> 5
68
+
69
+ ### Useful methods in the block
70
+
71
+ The last argument given to the block passed to new is the signal itself. This is
72
+ so you utilize other methods to query the signal and modify it's changed value.
73
+
74
+ {Oscillo::Signal#abort} aborts the new value change, keeping the old one.
75
+
76
+ a = Oscillo::Signal.new
77
+ b = Oscillo::Signal.new(a) { |v, s| s.abort if v == :bad; v }
78
+
79
+ a << :good
80
+ b.val #=> :good
81
+ a << :bad
82
+ b.val #=> :bad
83
+
84
+ {Oscillo::Signal#source} gives the original signal that caused the cascade of
85
+ changes. This is useful if you are following multiple signals and want to know
86
+ which one actually changed.
87
+
88
+ a = Oscillo::Signal.new
89
+ b = Oscillo::Signal.new
90
+ c = Oscillo::Signal.new(a, b) do |v1, v2, s|
91
+ "The last change was to #{s.source.val}"
92
+ end
93
+
94
+ a << 1
95
+ c.val #=> "The last change was to 1"
96
+ b << 2
97
+ c.val #=> "The last change was to 2"
98
+
99
+ ### Combining signals
100
+
101
+ You can combine signals together in different ways. For example,
102
+ {Oscillo::Combine.either} updates the new signal to the value of whichever was
103
+ the last signal to change.
104
+
105
+ a = Oscillo::Signal.new
106
+ b = Oscillo::Signal.new
107
+ c = Oscillo::Combine.either(a, b)
108
+ a << "a changed"
109
+ c.val #=> "a changed"
110
+ b << "b changed"
111
+ c.val #=> "b changed"
112
+
113
+ See {Oscillo::Combine} for all combination methods.
114
+
115
+ ### Enumerable
116
+
117
+ A signal can thought of a sequence of values over time. Therefore, {Oscillo::Signal}
118
+ implements a large number of Enumerable's methods. For example,
119
+
120
+ a = Oscillo::Signal.new(0)
121
+ b = a.map { |v| v ** 2 }
122
+ a << 3
123
+ b.val #=> 9
124
+
125
+ See {Oscillo::Enumerable} for all the methods implemented.
126
+
127
+ ## Contributing
128
+
129
+ 1. Fork it
130
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
131
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
132
+ 4. Push to the branch (`git push origin my-new-feature`)
133
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+ require 'rake/testtask'
4
+ require 'yard'
5
+
6
+ Rake::TestTask.new do |t|
7
+ t.libs << 'test'
8
+ end
9
+
10
+ YARD::Rake::YardocTask.new(:doc) do |t|
11
+ end
12
+
13
+ desc "Run tests"
14
+ task default: :test
@@ -0,0 +1,76 @@
1
+ module Oscillo
2
+ # A signal that operates on boolean values.
3
+ class BoolSignal < Signal
4
+ # (see Signal#initialize)
5
+ # Defaults to false instead of nil.
6
+ def initialize(*, &block)
7
+ @value = false; super
8
+ end
9
+
10
+ # Negates the signal. i.e if the original signal was true, the new signal is
11
+ # false and vice versa.
12
+ #
13
+ # @return [Signal]
14
+ def not
15
+ self.class.new(self) { |v| !v }
16
+ end
17
+ alias_method '!', :not
18
+
19
+ # The new signal is true if all of the given signals are true, otherwise it
20
+ # is false.
21
+ #
22
+ # @param signals [[Signal]]
23
+ # @return [Signal]
24
+ def and(*signals)
25
+ self.class.new(self, *signals) { |*vs, s| vs.all? }
26
+ end
27
+ alias_method :&, :and
28
+
29
+ # The new signal is true if all of the given signals are false, otherwise it
30
+ # is true.
31
+ #
32
+ # @param signals [[Signal]]
33
+ # @return [Signal]
34
+ def nand(*signals)
35
+ self.and(*signals).not
36
+ end
37
+
38
+ # The new signal is true if any of the given signals are true, otherwise it
39
+ # is false.
40
+ #
41
+ # @param signals [[Signal]]
42
+ # @return [Signal]
43
+ def or(*signals)
44
+ self.class.new(self, *signals) { |*vs, s| vs.any? }
45
+ end
46
+ alias_method :|, :or
47
+
48
+ # The new signal is true if any of the given signals are false, otherwise it
49
+ # is false.
50
+ #
51
+ # @param signals [[Signal]]
52
+ # @return [Signal]
53
+ def nor(*signals)
54
+ self.or(*signals).not
55
+ end
56
+
57
+ # The new signal is true if only one of the given signals are true,
58
+ # otherwise it is false.
59
+ #
60
+ # @param signal [Signal]
61
+ # @return [Signal]
62
+ def xor(signal)
63
+ self.class.new(self, signal) { |a, b| a ^ b }
64
+ end
65
+ alias_method :^, :xor
66
+
67
+ # The new signal is true if only one of the given signals are false,
68
+ # otherwise it is false.
69
+ #
70
+ # @param signal [Signal]
71
+ # @return [Signal]
72
+ def xnor(signal)
73
+ self.xor(signal).not
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,44 @@
1
+ module Oscillo
2
+ # Constructs new signals by combining multiple signals together in different
3
+ # ways.
4
+ module Combine
5
+ # When any of the signals change, update the new signal with the value of
6
+ # the last changed signal.
7
+ #
8
+ # @param signals [[Signal]] the signals to update on
9
+ # @return [Signal]
10
+ def self.either(*signals)
11
+ Signal.new(*signals) do |*vs, s|
12
+ s.source.val
13
+ end
14
+ end
15
+
16
+ # Updates the signal when any of the given signals change and the block
17
+ # evaluates to true. The value is the value of the last changed signal.
18
+ # @see when_not
19
+ #
20
+ # @param signals [[Signal]] the signals to update on
21
+ # @yield [value] the new value of the last changed signal
22
+ # @return [Signal]
23
+ def self.when(*signals, &block)
24
+ Signal.new(*signals) do |*vs, s|
25
+ s.abort unless block.(s.source.val)
26
+ s.source.val
27
+ end
28
+ end
29
+
30
+ # Updates the signal when any of the given signals change and the block
31
+ # evaluates to false. The value is the value of the last changed signal.
32
+ # @see when
33
+ #
34
+ # @param signals [[Signal]] the signals to update on
35
+ # @yield [value] the new value of the last changed signal
36
+ # @return [Signal]
37
+ def self.when_not(*signals, &block)
38
+ Signal.new(*signals) do |*vs, s|
39
+ s.abort if block.(s.source.val)
40
+ s.source.val
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,183 @@
1
+ module Oscillo
2
+ # Adds enumerable-like methods to Signal.
3
+ module Enumerable
4
+ NO_ARG = Object.new
5
+ private_constant :NO_ARG
6
+
7
+ # Returns a signal that is the result of running the values of the original
8
+ # signal through the block.
9
+ #
10
+ # @yield [*signals] gives the new values of the signals to the block
11
+ # @return [Signal]
12
+ def collect(&block)
13
+ self.class.new(self) { |*v, s| block.(*v) }
14
+ end
15
+ alias_method :map, :collect
16
+
17
+ # Returns a signal that changes only when the original signal changes and
18
+ # the block is not false.
19
+ # @see #reject
20
+ #
21
+ # @yield [value] gives the new value of the signal to the block
22
+ # @return [Signal]
23
+ def find_all(&block)
24
+ self.class.new(self) do |v, s|
25
+ s.abort unless block.(v); v
26
+ end
27
+ end
28
+ alias_method :select, :find_all
29
+
30
+ # Returns a signal that changes only when the original signal changes and
31
+ # the block is false.
32
+ # @see #find_all
33
+ #
34
+ # @yield [value] give the new value of the signal to the block
35
+ # @return [Signal]
36
+ def reject(&block)
37
+ self.class.new(self) do |v, s|
38
+ s.abort if block.(v); v
39
+ end
40
+ end
41
+
42
+ # Passes each value of the signal to the given block. The value of the
43
+ # resulting signal is true if the block never returns false or nil. If the
44
+ # block is not given, uses an implicit block of {|value| value} (that is
45
+ # {#all?} will return true only if none of the signal values were false or
46
+ # nil.)
47
+ # @note Only takes in to consideration values from the time the signal was
48
+ # created.
49
+ # @see #any?
50
+ #
51
+ # @yield [new_value] give the new value of the signal to the block
52
+ # @return [Signal<Boolean>]
53
+ def all?(&block)
54
+ self.class.new(true, self) do |v, s|
55
+ v = block.(v) if block_given?
56
+ s.val & v
57
+ end
58
+ end
59
+
60
+ # Passes each value of the signal to the given block. The value of the
61
+ # resulting signal is true if the block ever returns a value other than
62
+ # false or nil. If the block is not given, uses an implicit block of
63
+ # {|value| value} (that is {#any?} will return true if at least one of the
64
+ # signal values were not false or nil.)
65
+ # @note Only takes in to consideration values from the time the signal was
66
+ # created.
67
+ # @see #all?
68
+ #
69
+ # @yield [new_value] give the new value of the signal to the block
70
+ # @return [Signal<Boolean>]
71
+ def any?(&block)
72
+ self.class.new(false, self) do |v, s|
73
+ s.val | block.(v)
74
+ end
75
+ end
76
+
77
+ # Take the value from this signal and merges it with the values of the given
78
+ # signals to form an array.
79
+ #
80
+ # @param signals [[Signal]] the other signals to merge in the array
81
+ # @return [Signal]
82
+ def zip(*signals)
83
+ self.class.new(self, *signals) {|*v, s| v }
84
+ end
85
+
86
+ # Equivalent to {Signal#on_change}
87
+ #
88
+ # @yield (see Signal#on_change)
89
+ # @return [Signal]
90
+ def each(&block)
91
+ self.on_change(&block)
92
+ end
93
+
94
+ # Returns the number values that the signal has had. If an argument is
95
+ # given, counts the number of values in the signal, for which equals to
96
+ # item. If a block is given, counts the number of values yielding a true
97
+ # value.
98
+ # @note Only takes in to consideration values from the time the signal was
99
+ # created.
100
+ #
101
+ # @overload count
102
+ # @return number of values that the signal had
103
+ # @overload count(obj)
104
+ # @param obj values to count
105
+ # @return number of values equal to obj that the signal had
106
+ # @overload count
107
+ # @yield [value] gives the new value to the block
108
+ # @return number of values for which the block evaluates to true
109
+ def count(obj=NO_ARG, &block)
110
+ return count_with_arg(obj) unless obj.equal?(NO_ARG)
111
+ return count_with_block(&block) if block_given?
112
+
113
+ self.class.new(0, self) do |v, s|
114
+ s.val + 1
115
+ end
116
+ end
117
+
118
+ # Combines all values of the signal by applying a binary operation,
119
+ # specified by a block or a symbol that names a method or operator. If you
120
+ # specify a block, then for each value in this signal the block is passed an
121
+ # accumulator value (memo) the new value. If you specify a symbol instead,
122
+ # then each new value will be passed to the named method of memo. In either
123
+ # case, the result becomes the new value for memo.
124
+ #
125
+ # @overload inject
126
+ # @yield [memo, value] gives the memo and the new value of the signal to the
127
+ # block
128
+ # @overload inject(symbol)
129
+ # @param symbol that represents method to call on memo
130
+ # @overload inject(initial)
131
+ # @param initial the start value of the memo
132
+ # @yield [memo, value] gives the memo and the new value of the signal to the
133
+ # block
134
+ # @overload inject(initial, symbol)
135
+ # @param initial the start value of the memo
136
+ # @param symbol that represents method to call on memo
137
+ # @return [Signal]
138
+ def inject(*args, &block)
139
+ if block_given?
140
+ inject_with_block(*args, &block)
141
+ else
142
+ inject_with_sym(*args)
143
+ end
144
+ end
145
+ alias_method :reduce, :inject
146
+
147
+ private
148
+
149
+ def count_with_arg(obj)
150
+ self.class.new(0, self) do |v, s|
151
+ obj == v ? s.val + 1 : s.val
152
+ end
153
+ end
154
+
155
+ def count_with_block(&block)
156
+ self.class.new(0, self) do |v, s|
157
+ block.(v) ? s.val + 1 : s.val
158
+ end
159
+ end
160
+
161
+ def inject_with_sym(initial=NO_ARG, sym)
162
+ first_run = true
163
+ self.class.new(initial, self) do |v, s|
164
+ if first_run and initial.equal?(NO_ARG)
165
+ first_run = false; v
166
+ else
167
+ s.val.send(sym, v)
168
+ end
169
+ end
170
+ end
171
+
172
+ def inject_with_block(initial=NO_ARG, &block)
173
+ first_run = true
174
+ self.class.new(initial, self) do |v, s|
175
+ if first_run and initial.equal?(NO_ARG)
176
+ first_run = false; v
177
+ else
178
+ block.(s.val, v)
179
+ end
180
+ end
181
+ end
182
+ end
183
+ end
@@ -0,0 +1,96 @@
1
+ module Oscillo
2
+ # A signal that works on numeric values.
3
+ class NumSignal < Signal
4
+ # (see Signal#initialize)
5
+ # Defaults to 0 instead of nil.
6
+ def initialize(*, &block)
7
+ @value = 0; super
8
+ end
9
+
10
+ # The new signal gives the running total of all values of the original
11
+ # signal.
12
+ #
13
+ # @return [Signal]
14
+ def sum
15
+ self.inject(:+)
16
+ end
17
+
18
+ # The new signal gives the difference between the previous value and the
19
+ # current value of the signal.
20
+ #
21
+ # @return [Signal]
22
+ def delta
23
+ prev = self.val
24
+ self.class.new(self) do |v, s|
25
+ v - prev.tap { prev = v }
26
+ end
27
+ end
28
+
29
+ # The new signal gives the running product of all values of the original
30
+ # signal.
31
+ #
32
+ # @return [Signal]
33
+ def product
34
+ self.inject(:*)
35
+ end
36
+
37
+ # The new signal is the sum of the original signal and the given signal.
38
+ #
39
+ # @param signal [Signal] the signal to sum
40
+ # @return [Signal]
41
+ def plus(signal)
42
+ self.class.new(self, signal) { |a, b| a + b}
43
+ end
44
+ alias_method :+, :plus
45
+
46
+ # The new signal is the difference of the original signal and the given
47
+ # signal.
48
+ #
49
+ # @param signal [Signal] the signal to subtract
50
+ # @return [Signal]
51
+ def minus(signal)
52
+ self.class.new(self, signal) { |a, b| a - b }
53
+ end
54
+ alias_method :-, :minus
55
+
56
+ # The new signal is the product of the original signal and the given
57
+ # signal.
58
+ #
59
+ # @param signal [Signal] the signal to multiply
60
+ # @return [Signal]
61
+ def times(signal)
62
+ self.class.new(self, signal) { |a, b| a * b }
63
+ end
64
+ alias_method :*, :times
65
+
66
+ # The new signal is the division of the original signal and the given
67
+ # signal.
68
+ #
69
+ # @param signal [Signal] the signal to divide
70
+ # @return [Signal]
71
+ def div(signal)
72
+ self.class.new(self, signal) { |a, b| a / b }
73
+ end
74
+ alias_method :/, :div
75
+
76
+ # The new signal is the modulus of the original signal and the given
77
+ # signal.
78
+ #
79
+ # @param signal [Signal] the signal to mod
80
+ # @return [Signal]
81
+ def mod(signal)
82
+ self.class.new(self, signal) { |a, b| a % b }
83
+ end
84
+ alias_method :%, :mod
85
+
86
+ # The new signal is the original signal raised to the power of the given
87
+ # signal.
88
+ #
89
+ # @param signal [Signal] the signal to raise to the power of
90
+ # @return [Signal]
91
+ def pow(signal)
92
+ self.class.new(self, signal) { |a, b| a ** b }
93
+ end
94
+ alias_method :**, :pow
95
+ end
96
+ end
@@ -0,0 +1,187 @@
1
+ require 'oscillo/enumerable'
2
+
3
+ module Oscillo
4
+ # A signal that changes over time. This change can either be explicit through
5
+ # the use of {#change} or {#<<}, or it can be implicit when any of the signals
6
+ # that this signal follows changes.
7
+ #
8
+ # @author Evan Tatarka <evan@tatarka.me>
9
+ class Signal
10
+ include Enumerable
11
+
12
+ # Creates a new Signal with an optional initial value, signals to follow,
13
+ # and block. The block converts the values from any signals that this one
14
+ # follows to the value of this signal. If no block is given, the value will
15
+ # be an array of all the values of the signals that it follows (or if it is
16
+ # just following one signal, the value of that signal).
17
+ #
18
+ # @overload initialize(*signals)
19
+ # Follows some number of other signals, changing when they change.
20
+ # @param signals [Signal] the signals to follow
21
+ # @yield [*signals, self] gives the new values of the signals to the block
22
+ #
23
+ # @overload initialize(start_value, *signals)
24
+ # Starts with an initial value and follows other signals when they change.
25
+ # @param start_value the initial value of this signal
26
+ # @param signals [Signal] the signals to follow
27
+ # @yield [*signals, self] gives the new values of the signals to the block
28
+ def initialize(*values, &block)
29
+ @on_change = []
30
+ @follows = []
31
+ @followed_by = []
32
+
33
+ @block = block || ->(*x, s) { x.size <= 1 ? x.first : x }
34
+
35
+ return if values.empty?
36
+
37
+ # first may be initial value instead of signal
38
+ first, *rest = *values
39
+ if first.is_a?(self.class)
40
+ follow(*values)
41
+ else
42
+ @value = first
43
+ follow(*rest)
44
+ end
45
+ end
46
+
47
+ # Follows additional signals, changing when those signals change. This is
48
+ # necessary to define mutually recursive signals.
49
+ # @note Immediately updates value.
50
+ #
51
+ # @param signals [Signal] the signals to follow
52
+ # @return [self]
53
+ def follow(*signals)
54
+ @follows += signals.each { |signal| signal.followed_by(self) }
55
+ update(self)
56
+
57
+ self
58
+ end
59
+
60
+ # Stops following the given signals.
61
+ # @note Immediately updates value.
62
+ #
63
+ # @param signals [Signal] the signals to stop following
64
+ # @return [self]
65
+ def dont_follow(*signals)
66
+ @follows -= signals.each { |signal| signal.not_followed_by(self) }
67
+ update(self)
68
+
69
+ self
70
+ end
71
+
72
+ # Updates the value if this signal to represent the values of the signals
73
+ # that it follows. This is called automatically when dependent signals
74
+ # change so you don't need to call this manually in most cases.
75
+ #
76
+ # @param source [Signal] The signal that initialized the update. If not
77
+ # passed, then this signal will be the source of the update.
78
+ def update(source=nil)
79
+ return if @follows.empty? # nothing to update
80
+
81
+ @source = source
82
+ @aborted = false
83
+ new_value = @block.(*@follows.map(&:value), self)
84
+ self.change(new_value) unless @aborted
85
+ end
86
+
87
+ # Returns the current value of the signal
88
+ #
89
+ # @return the current value
90
+ def value
91
+ @value
92
+ end
93
+ alias_method :val, :value
94
+
95
+ # Change the current value of the signal. This will update all signals that
96
+ # follow this on as well as call all {#on_change} callbacks registered.
97
+ #
98
+ # @param new_value the new value of the signal
99
+ # @return [self]
100
+ def change(new_value)
101
+ @value = new_value
102
+ @on_change.each { |c| c.(new_value) }
103
+
104
+ unless self === source
105
+ @followed_by.each { |f| f.update(source || self) }
106
+ end
107
+ @source = nil
108
+
109
+ self
110
+ end
111
+ alias_method :<<, :change
112
+
113
+ # Register a callback that will be called any time the signal value is
114
+ # changed.
115
+ #
116
+ # @yield [value] gives the new value to the block
117
+ # @return [self]
118
+ def on_change(&block)
119
+ @on_change << block
120
+
121
+ self
122
+ end
123
+
124
+ # Aborts the change so that the value stays the same, no callbacks
125
+ # registered by {#on_change} are called, and no dependent signals are
126
+ # changed.
127
+ # @note This should only be called in the block passed to {#initialize}.
128
+ def abort
129
+ @aborted = true
130
+ end
131
+
132
+ # The original source of the current run of signal changes. This can be
133
+ # useful to know which signal caused the change if you are following
134
+ # multiple signals.
135
+ # @note This is only defined within the block passed to {#initialize}.
136
+ #
137
+ # @return [Signal] the signal that caused the change
138
+ def source
139
+ @source
140
+ end
141
+
142
+ # The value of the signal as a string, for convenience.
143
+ #
144
+ # @return [String] the signal value
145
+ def to_s
146
+ value.to_s
147
+ end
148
+
149
+ # The value of the signal as an integer, for convenience.
150
+ #
151
+ # @return [Fixnum] the signal value
152
+ def to_i
153
+ value.to_i
154
+ end
155
+
156
+ # The new signal ignores change events when the new value is the same as the
157
+ # old value.
158
+ #
159
+ # @return [Signal]
160
+ def drop_repeats
161
+ self.class.new(self) do |v, s|
162
+ s.abort if s.val == v; v
163
+ end
164
+ end
165
+
166
+ # Updates the new signal to the value of this signal any time the value of
167
+ # sample changes.
168
+ #
169
+ # @param sample [Signal] the signal to update on
170
+ # @return [Signal]
171
+ def sample_on(sample)
172
+ Signal.new(self.val, sample) do |v, s|
173
+ self.val
174
+ end
175
+ end
176
+
177
+ protected
178
+
179
+ def followed_by(follower)
180
+ @followed_by << follower
181
+ end
182
+
183
+ def not_followed_by(follower)
184
+ @followed_by.delete(follower)
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,3 @@
1
+ module Oscillo
2
+ VERSION = "0.0.1"
3
+ end
data/lib/oscillo.rb ADDED
@@ -0,0 +1,8 @@
1
+ require "oscillo/version"
2
+
3
+ module Oscillo
4
+ require 'oscillo/signal'
5
+ require 'oscillo/combine'
6
+ require 'oscillo/bool_signal'
7
+ require 'oscillo/num_signal'
8
+ end
data/oscillo.gemspec ADDED
@@ -0,0 +1,20 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/oscillo/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Evan Tatarka"]
6
+ gem.email = ["evan@tatarka.me"]
7
+ gem.description = %q{Modify and respond to signals, inspired by functional reactive programming}
8
+ gem.summary = %q{Modify and respond to signals, inspired by functional reactive programming}
9
+ gem.homepage = "http://www.github.com/evant/oscillo"
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = "oscillo"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = Oscillo::VERSION
17
+
18
+ gem.add_development_dependency 'yard', '~> 0.8.3'
19
+ gem.add_development_dependency 'redcarpet', '~> 2.2.2'
20
+ end
@@ -0,0 +1,44 @@
1
+ require 'test/unit'
2
+ require 'oscillo'
3
+
4
+ class TestBoolSignal < Test::Unit::TestCase
5
+ include Oscillo
6
+
7
+ def self.test_bool(method, expected)
8
+ a = BoolSignal.new
9
+ b = BoolSignal.new
10
+ c = a.send(method, b)
11
+
12
+ define_method("test_#{method}") do
13
+ i = 0
14
+ [true, false].each do |a_val|
15
+ [true, false].each do |b_val|
16
+ a << a_val; b << b_val
17
+ assert_equal expected[i], c.val
18
+ i += 1
19
+ end
20
+ end
21
+ end
22
+ end
23
+
24
+ def test_default_value
25
+ assert_equal false, BoolSignal.new.val
26
+ end
27
+
28
+ def test_not
29
+ a = BoolSignal.new
30
+ b = a.not
31
+
32
+ a << true
33
+ assert_equal false, b.val
34
+ a << false
35
+ assert_equal true, b.val
36
+ end
37
+
38
+ test_bool :and, [true, false, false, false]
39
+ test_bool :nand, [false, true, true, true ]
40
+ test_bool :or, [true, true, true, false]
41
+ test_bool :nor, [false, false, false, true ]
42
+ test_bool :xor, [false, true, true, false]
43
+ test_bool :xnor, [true, false, false, true ]
44
+ end
@@ -0,0 +1,47 @@
1
+ require 'test/unit'
2
+ require 'oscillo'
3
+
4
+ class TestCombine < Test::Unit::TestCase
5
+ include Oscillo
6
+
7
+ def test_either
8
+ a = Signal.new
9
+ b = Signal.new
10
+ c = Combine::either(a, b)
11
+
12
+ a << :a
13
+ assert_equal :a, c.val
14
+ b << :b
15
+ assert_equal :b, c.val
16
+ end
17
+
18
+ def test_when
19
+ a = Signal.new
20
+ b = Signal.new
21
+ c = Combine::when(a, b) { |x| x != :bad }
22
+
23
+ a << :a
24
+ assert_equal :a, c.val
25
+ a << :bad
26
+ assert_equal :a, c.val
27
+ b << :b
28
+ assert_equal :b, c.val
29
+ b << :bad
30
+ assert_equal :b, c.val
31
+ end
32
+
33
+ def test_when_not
34
+ a = Signal.new
35
+ b = Signal.new
36
+ c = Combine::when_not(a, b) { |x| x == :bad }
37
+
38
+ a << :a
39
+ assert_equal :a, c.val
40
+ a << :bad
41
+ assert_equal :a, c.val
42
+ b << :b
43
+ assert_equal :b, c.val
44
+ b << :bad
45
+ assert_equal :b, c.val
46
+ end
47
+ end
@@ -0,0 +1,137 @@
1
+ require 'test/unit'
2
+ require 'oscillo'
3
+
4
+ class TestEnumerable < Test::Unit::TestCase
5
+ include Oscillo
6
+
7
+ def test_collect
8
+ a = Signal.new(0)
9
+ b = a.map { |x| x * 2 }
10
+
11
+ a << 2
12
+ assert_equal 2 * 2, b.val
13
+ end
14
+
15
+ def test_find_all
16
+ a = Signal.new
17
+ b = a.find_all { |x| x == :good }
18
+
19
+ a << :good
20
+ assert_equal :good, b.val
21
+ a << :bad
22
+ assert_equal :good, b.val
23
+ end
24
+
25
+ def test_select
26
+ test_find_all
27
+ end
28
+
29
+ def test_reject
30
+ a = Signal.new
31
+ b = a.reject { |x| x == :bad }
32
+
33
+ a << :good
34
+ assert_equal :good, b.val
35
+ b << :bad
36
+ assert_equal :bad, b.val
37
+ end
38
+
39
+ def test_all?
40
+ a = Signal.new(:good)
41
+ b = a.all? { |x| x == :good }
42
+
43
+ assert b.val
44
+ a << :bad
45
+ assert !b.val
46
+ a << :good
47
+ assert !b.val
48
+ end
49
+
50
+ def test_any?
51
+ a = Signal.new
52
+ b = a.any? { |x| x == :good }
53
+
54
+ a << :bad
55
+ assert !b.val
56
+ a << :good
57
+ assert b.val
58
+ a << :bad
59
+ assert b.val
60
+ end
61
+
62
+ def test_zip
63
+ a = Signal.new
64
+ b = Signal.new
65
+ c = a.zip(b)
66
+
67
+ a << :a
68
+ b << :b
69
+ assert_equal [:a, :b], c.val
70
+ end
71
+
72
+ def test_inject_with_block
73
+ a = Signal.new("a")
74
+ b = a.inject { |str, value| str + value }
75
+
76
+ assert_equal "a", b.val
77
+ a << "b"
78
+ assert_equal "ab", b.val
79
+ end
80
+
81
+ def test_inject_with_symbol
82
+ a = Signal.new(0)
83
+ b = a.inject(:+)
84
+
85
+ a << 1
86
+ assert_equal 1, b.val
87
+ a << 2
88
+ assert_equal 3, b.val
89
+ end
90
+
91
+ def test_inject_with_initial_and_block
92
+ a = Signal.new(:a)
93
+ b = a.inject([]) { |array, value| array << value }
94
+
95
+ assert_equal [:a], b.val
96
+ a << :b
97
+ assert_equal [:a, :b], b.val
98
+ end
99
+
100
+ def test_inject_with_initial_and_symbol
101
+ a = Signal.new(1)
102
+ b = a.inject(0, :+)
103
+
104
+ assert_equal 1, b.val
105
+ a << 2
106
+ assert_equal 3, b.val
107
+ end
108
+
109
+ def test_count
110
+ a = Signal.new(:a)
111
+ b = a.count
112
+
113
+ assert_equal 1, b.val
114
+ a << :b
115
+ assert_equal 2, b.val
116
+ end
117
+
118
+ def test_count_with_arg
119
+ a = Signal.new
120
+ b = a.count(:good)
121
+
122
+ a << :good
123
+ assert_equal 1, b.val
124
+ a << :bad
125
+ assert_equal 1, b.val
126
+ end
127
+
128
+ def test_count_with_block
129
+ a = Signal.new
130
+ b = a.count { |x| x == :good }
131
+
132
+ a << :good
133
+ assert_equal 1, b.val
134
+ a << :bad
135
+ assert_equal 1, b.val
136
+ end
137
+ end
@@ -0,0 +1,96 @@
1
+ require 'test/unit'
2
+ require 'oscillo'
3
+
4
+ class TestNumSignal < Test::Unit::TestCase
5
+ include Oscillo
6
+
7
+ def test_default_value
8
+ assert_equal 0, NumSignal.new.val
9
+ end
10
+
11
+ def test_sum
12
+ a = NumSignal.new(0)
13
+ b = a.sum
14
+
15
+ a << 1
16
+ assert_equal 1, b.val
17
+ a << 2
18
+ assert_equal 3, b.val
19
+ end
20
+
21
+ def test_delta
22
+ a = NumSignal.new(0)
23
+ b = a.delta
24
+
25
+ a << 5
26
+ assert_equal 5, b.val
27
+ a << 2
28
+ assert_equal(-3, b.val)
29
+ a << 4
30
+ assert_equal 2, b.val
31
+ end
32
+
33
+ def test_product
34
+ a = NumSignal.new(1)
35
+ b = a.product
36
+
37
+ a << 2
38
+ assert_equal 2, b.val
39
+ a << 3
40
+ assert_equal 6, b.val
41
+ end
42
+
43
+ def test_plus
44
+ a = NumSignal.new
45
+ b = NumSignal.new
46
+ c = a + b
47
+
48
+ a << 1; b << 2
49
+ assert_equal 3, c.val
50
+ end
51
+
52
+ def test_minus
53
+ a = NumSignal.new
54
+ b = NumSignal.new
55
+ c = a - b
56
+
57
+ a << 5; b << 2
58
+ assert_equal 3, c.val
59
+ end
60
+
61
+ def test_mult
62
+ a = NumSignal.new
63
+ b = NumSignal.new
64
+ c = a * b
65
+
66
+ a << 2; b << 3
67
+ assert_equal 6, c.val
68
+ end
69
+
70
+ def test_div
71
+ a = NumSignal.new
72
+ b = NumSignal.new(1) # Prevent division by 0
73
+ c = a / b
74
+
75
+ a << 6; b << 3
76
+ assert_equal 2, c.val
77
+ end
78
+
79
+ def test_mod
80
+ a = NumSignal.new
81
+ b = NumSignal.new(1) # Prevent division by 0
82
+ c = a % b
83
+
84
+ a << 12; b << 5
85
+ assert_equal 2, c.val
86
+ end
87
+
88
+ def test_pow
89
+ a = NumSignal.new
90
+ b = NumSignal.new
91
+ c = a ** b
92
+
93
+ a << 2; b << 3
94
+ assert_equal 8, c.val
95
+ end
96
+ end
@@ -0,0 +1,145 @@
1
+ require 'test/unit'
2
+ require 'oscillo'
3
+
4
+ class TestSignal < Test::Unit::TestCase
5
+ include Oscillo
6
+
7
+ def test_initial_value
8
+ assert_equal :test, Signal.new(:test).val
9
+ end
10
+
11
+ def test_change_value
12
+ a = Signal.new
13
+ a << :test
14
+
15
+ assert_equal :test, a.val
16
+ end
17
+
18
+ def test_follows
19
+ a = Signal.new
20
+ b = Signal.new(a)
21
+
22
+ a << :test
23
+ assert_equal :test, b.val
24
+ end
25
+
26
+ def test_follows_initial_value
27
+ a = Signal.new(:a)
28
+ b = Signal.new(a)
29
+
30
+ assert_equal :a, b.val
31
+ end
32
+
33
+ def test_follows_with_block
34
+ a = Signal.new(0)
35
+ b = Signal.new(a) { |x| x * 2 }
36
+
37
+ a << 2
38
+ assert_equal 2 * 2, b.val
39
+ end
40
+
41
+ def test_follows_initial_value_with_block
42
+ a = Signal.new(2)
43
+ b = Signal.new(a) { |x| x * 2 }
44
+
45
+ assert_equal 2 * 2, b.val
46
+ end
47
+
48
+ def test_follows_multiple
49
+ a = Signal.new
50
+ b = Signal.new
51
+ c = Signal.new(a, b)
52
+
53
+ a << :test1
54
+ b << :test2
55
+ assert_equal [:test1, :test2], c.val
56
+ end
57
+
58
+ def test_follows_multiple_with_block
59
+ a = Signal.new(0)
60
+ b = Signal.new(0)
61
+ c = Signal.new(a, b) { |x, y| x + y }
62
+
63
+ a << 2
64
+ b << 3
65
+ assert_equal 2 + 3, c.val
66
+ end
67
+
68
+ def test_abort_follows
69
+ a = Signal.new
70
+ b = Signal.new(a) { |x| b.abort if x == :bad; x }
71
+
72
+ a << :good
73
+ assert_equal :good, b.val
74
+ a << :bad
75
+ assert_equal :good, b.val
76
+ end
77
+
78
+ def test_chained_follows
79
+ a = Signal.new
80
+ b = Signal.new(a)
81
+ c = Signal.new(b)
82
+
83
+ a << :test
84
+ assert_equal :test, b.val
85
+ assert_equal :test, c.val
86
+ end
87
+
88
+ def test_recursive_follows
89
+ a = Signal.new
90
+ b = Signal.new(a)
91
+
92
+ # should not cause a SystemStackError
93
+ a.follow(b)
94
+
95
+ a << :test_a
96
+ assert_equal :test_a, b.val
97
+ b << :test_b
98
+ assert_equal :test_b, a.val
99
+ end
100
+
101
+ def test_dont_follow
102
+ a = Signal.new
103
+ b = Signal.new(a)
104
+ b.dont_follow(a)
105
+
106
+ a << :test
107
+ assert :test != b.val
108
+ end
109
+
110
+ def test_on_change
111
+ a = Signal.new(:a)
112
+ changes = 0
113
+ a.on_change { changes += 1 }
114
+
115
+ assert_equal 0, changes
116
+ a << :a1
117
+ assert_equal 1, changes
118
+ end
119
+
120
+ def test_sample_on
121
+ a = Signal.new(:start)
122
+ b = Signal.new
123
+ c = a.sample_on(b)
124
+
125
+ a << :new
126
+ assert_equal :start, c.val
127
+ b << :changed
128
+ assert_equal :new, c.val
129
+ end
130
+
131
+ def test_drop_repeats
132
+ a = Signal.new
133
+ b = a.drop_repeats
134
+
135
+ changes = 0
136
+ b.on_change { changes += 1 }
137
+
138
+ a << :a
139
+ assert_equal 1, changes
140
+ a << :a
141
+ assert_equal 1, changes
142
+ a << :b
143
+ assert_equal 2, changes
144
+ end
145
+ end
metadata ADDED
@@ -0,0 +1,91 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: oscillo
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Evan Tatarka
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-11-20 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: yard
16
+ requirement: &13298160 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 0.8.3
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: *13298160
25
+ - !ruby/object:Gem::Dependency
26
+ name: redcarpet
27
+ requirement: &13297620 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ~>
31
+ - !ruby/object:Gem::Version
32
+ version: 2.2.2
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: *13297620
36
+ description: Modify and respond to signals, inspired by functional reactive programming
37
+ email:
38
+ - evan@tatarka.me
39
+ executables: []
40
+ extensions: []
41
+ extra_rdoc_files: []
42
+ files:
43
+ - .gitignore
44
+ - Gemfile
45
+ - LICENSE
46
+ - README.md
47
+ - Rakefile
48
+ - lib/oscillo.rb
49
+ - lib/oscillo/bool_signal.rb
50
+ - lib/oscillo/combine.rb
51
+ - lib/oscillo/enumerable.rb
52
+ - lib/oscillo/num_signal.rb
53
+ - lib/oscillo/signal.rb
54
+ - lib/oscillo/version.rb
55
+ - oscillo.gemspec
56
+ - test/test_bool_signal.rb
57
+ - test/test_combine.rb
58
+ - test/test_enumerable.rb
59
+ - test/test_num_signal.rb
60
+ - test/test_signal.rb
61
+ homepage: http://www.github.com/evant/oscillo
62
+ licenses: []
63
+ post_install_message:
64
+ rdoc_options: []
65
+ require_paths:
66
+ - lib
67
+ required_ruby_version: !ruby/object:Gem::Requirement
68
+ none: false
69
+ requirements:
70
+ - - ! '>='
71
+ - !ruby/object:Gem::Version
72
+ version: '0'
73
+ required_rubygems_version: !ruby/object:Gem::Requirement
74
+ none: false
75
+ requirements:
76
+ - - ! '>='
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ requirements: []
80
+ rubyforge_project:
81
+ rubygems_version: 1.8.11
82
+ signing_key:
83
+ specification_version: 3
84
+ summary: Modify and respond to signals, inspired by functional reactive programming
85
+ test_files:
86
+ - test/test_bool_signal.rb
87
+ - test/test_combine.rb
88
+ - test/test_enumerable.rb
89
+ - test/test_num_signal.rb
90
+ - test/test_signal.rb
91
+ has_rdoc: