oscillo 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.
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: