dataflow 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/History.txt +5 -0
- data/LICENSE +19 -0
- data/README.textile +172 -0
- data/Rakefile +35 -0
- data/dataflow.rb +90 -0
- data/examples/data_driven.rb +17 -0
- data/examples/dataflow_http_gets.rb +13 -0
- data/examples/future_http_gets.rb +12 -0
- data/examples/instance_variables.rb +15 -0
- data/examples/laziness.rb +9 -0
- data/examples/local_variables.rb +11 -0
- data/examples/messages.rb +26 -0
- data/examples/port_http_gets.rb +13 -0
- data/examples/port_send.rb +10 -0
- data/examples/ring.rb +21 -0
- data/lib/actor.rb +22 -0
- data/lib/port.rb +54 -0
- data/spec/actor_spec.rb +29 -0
- data/spec/by_need_spec.rb +12 -0
- data/spec/dataflow_spec.rb +128 -0
- data/spec/need_later_spec.rb +12 -0
- data/spec/port_spec.rb +26 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +7 -0
- metadata +75 -0
data/History.txt
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (c) 2009 Larry Diehl
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
THE SOFTWARE.
|
data/README.textile
ADDED
@@ -0,0 +1,172 @@
|
|
1
|
+
h1. What's this?
|
2
|
+
|
3
|
+
A Ruby library that adds Dataflow variables (inspired by the Oz
|
4
|
+
language). Dataflow variables have the property that they can only
|
5
|
+
be bound/assigned to once, or have an equivalent value as an existing
|
6
|
+
assignment (see "unification").
|
7
|
+
|
8
|
+
Dataflow variables must be declared before they are used, and can be
|
9
|
+
passed around as data without actually being bound. If the variable
|
10
|
+
gets used (in this library this means a method call) while being
|
11
|
+
unbound then the currently executing thread will suspend.
|
12
|
+
|
13
|
+
h1. What's the point?
|
14
|
+
|
15
|
+
Ruby is Object Oriented (with the ability to mutate local, instance,
|
16
|
+
class, and global variables, and even constants), and on top of that
|
17
|
+
it has powerful reflection and meta-programming abilities. While these
|
18
|
+
features are useful for certain problems, they are not within the
|
19
|
+
declarative model. Staying in the declarative model gives one 2 advantages:
|
20
|
+
# It is easy to reason about what the program does
|
21
|
+
# Simple but powerful concurrency is possible
|
22
|
+
|
23
|
+
Ruby, like many other OO languages, is facing the hurdles of taking
|
24
|
+
advantage of the increase of processor cores within a simple parallel
|
25
|
+
programming model. This library lets you program Ruby in the
|
26
|
+
declarative concurrent model when you need to take advantage of multiple cores
|
27
|
+
(assuming a Ruby implementation that uses native threads in one way or
|
28
|
+
another).
|
29
|
+
|
30
|
+
The trick to this kind of programming is binding variables from other
|
31
|
+
threads. The nice thing is that many existing
|
32
|
+
libraries/classes/methods can still be used, just avoid
|
33
|
+
side-effects. Use regular Ruby threading to create threads, use
|
34
|
+
"local" or "declare" to create new variables, and use "unify" to bind
|
35
|
+
variables.
|
36
|
+
|
37
|
+
h1. Examples
|
38
|
+
|
39
|
+
<pre>
|
40
|
+
# Local variables
|
41
|
+
include Dataflow
|
42
|
+
|
43
|
+
local do |x, y, z|
|
44
|
+
# notice how the order automatically gets resolved
|
45
|
+
Thread.new { unify y, x + 2 }
|
46
|
+
Thread.new { unify z, y + 3 }
|
47
|
+
Thread.new { unify x, 1 }
|
48
|
+
z #=> 6
|
49
|
+
end
|
50
|
+
</pre>
|
51
|
+
|
52
|
+
<pre>
|
53
|
+
# Instance variables
|
54
|
+
class AnimalHouse
|
55
|
+
include Dataflow
|
56
|
+
declare :small_cat, :big_cat
|
57
|
+
|
58
|
+
def fetch_big_cat
|
59
|
+
Thread.new { unify big_cat, small_cat.upcase }
|
60
|
+
unify small_cat, 'cat'
|
61
|
+
big_cat
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
AnimalHouse.new.fetch_big_cat #=> 'CAT'
|
66
|
+
</pre>
|
67
|
+
|
68
|
+
<pre>
|
69
|
+
# Data-driven concurrency
|
70
|
+
include Dataflow
|
71
|
+
|
72
|
+
local do |stream, doubles, triples, squares|
|
73
|
+
unify stream, Array.new(5) { Dataflow::Variable.new }
|
74
|
+
|
75
|
+
Thread.new { unify doubles, stream.map {|n| n*2 } }
|
76
|
+
Thread.new { unify triples, stream.map {|n| n*3 } }
|
77
|
+
Thread.new { unify squares, stream.map {|n| n**2 } }
|
78
|
+
|
79
|
+
Thread.new { stream.each {|x| unify x, rand(100) } }
|
80
|
+
|
81
|
+
puts "original: #{stream.inspect}"
|
82
|
+
puts "doubles: #{doubles.inspect}"
|
83
|
+
puts "triples: #{triples.inspect}"
|
84
|
+
puts "squares: #{squares.inspect}"
|
85
|
+
end
|
86
|
+
</pre>
|
87
|
+
|
88
|
+
<pre>
|
89
|
+
# By-need trigger laziness
|
90
|
+
include Dataflow
|
91
|
+
|
92
|
+
local do |x, y, z|
|
93
|
+
Thread.new { unify y, by_need { 4 } }
|
94
|
+
Thread.new { unify z, x + y }
|
95
|
+
Thread.new { unify x, by_need { 3 } }
|
96
|
+
z #=> 7
|
97
|
+
end
|
98
|
+
</pre>
|
99
|
+
|
100
|
+
<pre>
|
101
|
+
# Need-later future expressions
|
102
|
+
include Dataflow
|
103
|
+
|
104
|
+
local do |x, y, z|
|
105
|
+
unify y, need_later { 4 }
|
106
|
+
unify z, need_later { x + y }
|
107
|
+
unify x, need_later { 3 }
|
108
|
+
z #=> 7
|
109
|
+
end
|
110
|
+
</pre>
|
111
|
+
|
112
|
+
h1. Ports using Dataflow
|
113
|
+
|
114
|
+
Ports are an extension of the declarative concurrent model to support nondeterministic behavior. They accomplish this through the use of a single state variable. Ports are also inspired by the Oz language.
|
115
|
+
|
116
|
+
An Actor class in the style of Erlang message-passing processes is also provided. It makes use of the asynchronous behavior of ports, but otherwise uses no state variables.
|
117
|
+
|
118
|
+
h1. Examples using Ports
|
119
|
+
|
120
|
+
<pre>
|
121
|
+
include Dataflow
|
122
|
+
|
123
|
+
local do |port, stream|
|
124
|
+
unify port, Dataflow::Port.new(stream)
|
125
|
+
Thread.new {port.send 2}
|
126
|
+
Thread.new {port.send 8}
|
127
|
+
Thread.new {port.send 1024}
|
128
|
+
stream.take(3).sort #=> [2, 8, 1024]
|
129
|
+
end
|
130
|
+
</pre>
|
131
|
+
|
132
|
+
h1. Examples using Actors
|
133
|
+
|
134
|
+
<pre>
|
135
|
+
include Dataflow
|
136
|
+
|
137
|
+
Ping = Actor.new {
|
138
|
+
3.times {
|
139
|
+
case receive
|
140
|
+
when "Ping"
|
141
|
+
puts "Ping"
|
142
|
+
Pong.send "Pong"
|
143
|
+
end
|
144
|
+
}
|
145
|
+
}
|
146
|
+
|
147
|
+
Pong = Actor.new {
|
148
|
+
3.times {
|
149
|
+
case receive
|
150
|
+
when "Pong"
|
151
|
+
puts "Pong"
|
152
|
+
Ping.send "Ping"
|
153
|
+
end
|
154
|
+
}
|
155
|
+
}
|
156
|
+
|
157
|
+
Actor.new { Ping.send "Ping" }
|
158
|
+
|
159
|
+
Ping.join
|
160
|
+
Pong.join
|
161
|
+
</pre>
|
162
|
+
|
163
|
+
h1. References
|
164
|
+
|
165
|
+
The basis of dataflow variables around a language is not common among
|
166
|
+
popular languages and may be confusing to some. For an in-depth
|
167
|
+
introduction to the Oz language and the techniques used in this
|
168
|
+
library (including by_need triggers, port objects, and comparisons to Erlang message passing) see the book "Concepts, Techniques, and Models of Computer Programming":http://en.wikipedia.org/wiki/Concepts,_Techniques,_and_Models_of_Computer_Programming
|
169
|
+
|
170
|
+
h1. Contributors
|
171
|
+
|
172
|
+
larrytheliquid, amiller
|
data/Rakefile
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
require "rubygems"
|
2
|
+
require "rake/gempackagetask"
|
3
|
+
require "rake/clean"
|
4
|
+
require "spec/rake/spectask"
|
5
|
+
require File.expand_path("./dataflow")
|
6
|
+
|
7
|
+
Spec::Rake::SpecTask.new do |t|
|
8
|
+
t.spec_opts = ['--options', "\"#{File.dirname(__FILE__)}/spec/spec.opts\""]
|
9
|
+
end
|
10
|
+
|
11
|
+
desc "Run the specs"
|
12
|
+
task :default => :spec
|
13
|
+
|
14
|
+
spec = Gem::Specification.new do |s|
|
15
|
+
s.name = "dataflow"
|
16
|
+
s.rubyforge_project = s.name
|
17
|
+
s.version = Dataflow::VERSION
|
18
|
+
s.author = "Larry Diehl"
|
19
|
+
s.email = "larrytheliquid" + "@" + "gmail.com"
|
20
|
+
s.homepage = "http://github.com/larrytheliquid/dataflow"
|
21
|
+
s.summary = "Dataflow concurrency for Ruby (inspired by the Oz language)"
|
22
|
+
s.description = s.summary
|
23
|
+
s.files = %w[LICENSE History.txt Rakefile README.textile dataflow.rb] + Dir["lib/**/*"] + Dir["examples/**/*"]
|
24
|
+
s.test_files = Dir["spec/**/*"]
|
25
|
+
end
|
26
|
+
|
27
|
+
Rake::GemPackageTask.new(spec) do |package|
|
28
|
+
package.gem_spec = spec
|
29
|
+
end
|
30
|
+
|
31
|
+
desc 'Install the package as a gem.'
|
32
|
+
task :install => [:clean, :package] do
|
33
|
+
gem = Dir['pkg/*.gem'].first
|
34
|
+
sh "sudo gem install --no-rdoc --no-ri --local #{gem}"
|
35
|
+
end
|
data/dataflow.rb
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
require 'monitor'
|
2
|
+
|
3
|
+
module Dataflow
|
4
|
+
VERSION = "0.1.0"
|
5
|
+
|
6
|
+
def self.included(cls)
|
7
|
+
class << cls
|
8
|
+
def declare(*readers)
|
9
|
+
readers.each do |name|
|
10
|
+
class_eval <<-RUBY
|
11
|
+
def #{name}
|
12
|
+
return @__dataflow_#{name}__ if defined? @__dataflow_#{name}__
|
13
|
+
Variable::LOCK.synchronize { @__dataflow_#{name}__ ||= Variable.new }
|
14
|
+
end
|
15
|
+
RUBY
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def local(&block)
|
22
|
+
vars = Array.new(block.arity) { Variable.new }
|
23
|
+
block.call *vars
|
24
|
+
end
|
25
|
+
|
26
|
+
def unify(variable, value)
|
27
|
+
variable.__unify__ value
|
28
|
+
end
|
29
|
+
|
30
|
+
def by_need(&block)
|
31
|
+
Variable.new &block
|
32
|
+
end
|
33
|
+
|
34
|
+
def need_later(&block)
|
35
|
+
local do |future|
|
36
|
+
Thread.new { unify future, block.call }
|
37
|
+
future
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Note that this class uses instance variables directly rather than nicely
|
42
|
+
# initialized instance variables in get/set methods for memory and
|
43
|
+
# performance reasons
|
44
|
+
class Variable
|
45
|
+
instance_methods.each { |m| undef_method m unless m =~ /^__/ }
|
46
|
+
LOCK = Monitor.new
|
47
|
+
def initialize(&block) @__trigger__ = block if block_given? end
|
48
|
+
|
49
|
+
# Lazy-load conditions to be nice on memory usage
|
50
|
+
def __binding_condition__() @__binding_condition__ ||= LOCK.new_cond end
|
51
|
+
|
52
|
+
def __unify__(value)
|
53
|
+
LOCK.synchronize do
|
54
|
+
if @__bound__
|
55
|
+
raise UnificationError if @__value__ != value
|
56
|
+
else
|
57
|
+
@__value__ = value
|
58
|
+
@__bound__ = true
|
59
|
+
__binding_condition__.broadcast # wakeup all method callers
|
60
|
+
remove_instance_variable :@__binding_condition__ # GC
|
61
|
+
end
|
62
|
+
end
|
63
|
+
@__value__
|
64
|
+
end
|
65
|
+
|
66
|
+
def __activate_trigger__
|
67
|
+
@__value__ = @__trigger__.call
|
68
|
+
@__bound__ = true
|
69
|
+
remove_instance_variable :@__trigger__ # GC
|
70
|
+
end
|
71
|
+
|
72
|
+
def method_missing(name, *args, &block)
|
73
|
+
LOCK.synchronize do
|
74
|
+
unless @__bound__
|
75
|
+
if @__trigger__
|
76
|
+
__activate_trigger__
|
77
|
+
else
|
78
|
+
__binding_condition__.wait
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end unless @__bound__
|
82
|
+
@__value__.__send__(name, *args, &block)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
UnificationError = Class.new StandardError
|
87
|
+
end
|
88
|
+
|
89
|
+
require "#{File.dirname(__FILE__)}/lib/port"
|
90
|
+
require "#{File.dirname(__FILE__)}/lib/actor"
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require "#{File.dirname(__FILE__)}/../dataflow"
|
2
|
+
include Dataflow
|
3
|
+
|
4
|
+
local do |stream, doubles, triples, squares|
|
5
|
+
unify stream, Array.new(5) { Dataflow::Variable.new }
|
6
|
+
|
7
|
+
Thread.new { unify doubles, stream.map {|n| n*2 } }
|
8
|
+
Thread.new { unify triples, stream.map {|n| n*3 } }
|
9
|
+
Thread.new { unify squares, stream.map {|n| n**2 } }
|
10
|
+
|
11
|
+
Thread.new { stream.each {|x| unify x, rand(100) } }
|
12
|
+
|
13
|
+
puts "original: #{stream.inspect}"
|
14
|
+
puts "doubles: #{doubles.inspect}"
|
15
|
+
puts "triples: #{triples.inspect}"
|
16
|
+
puts "squares: #{squares.inspect}"
|
17
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require "#{File.dirname(__FILE__)}/../dataflow"
|
2
|
+
require 'net/http'
|
3
|
+
include Dataflow
|
4
|
+
|
5
|
+
# Be gentle running this one
|
6
|
+
Thread.abort_on_exception = true
|
7
|
+
local do |stream, branding_occurences, number_of_mentions|
|
8
|
+
unify stream, Array.new(10) { Dataflow::Variable.new }
|
9
|
+
stream.each {|s| Thread.new(s) {|v| unify v, Net::HTTP.get_response(URI.parse("http://www.cuil.com/search?q=#{rand(1000)}")).body } }
|
10
|
+
Thread.new { unify branding_occurences, stream.map {|http_body| http_body.scan /cuil/ } }
|
11
|
+
Thread.new { unify number_of_mentions, branding_occurences.map {|occurences| occurences.length } }
|
12
|
+
puts number_of_mentions.inspect
|
13
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require "#{File.dirname(__FILE__)}/../dataflow"
|
2
|
+
require 'net/http'
|
3
|
+
include Dataflow
|
4
|
+
|
5
|
+
# Be gentle running this one
|
6
|
+
Thread.abort_on_exception = true
|
7
|
+
local do |stream, branding_occurences, number_of_mentions|
|
8
|
+
unify stream, Array.new(10) { need_later { Net::HTTP.get_response(URI.parse("http://www.cuil.com/search?q=#{rand(1000)}")).body } }
|
9
|
+
unify branding_occurences, need_later { stream.map {|http_body| http_body.scan /cuil/ } }
|
10
|
+
unify number_of_mentions, need_later { branding_occurences.map {|occurences| occurences.length } }
|
11
|
+
puts number_of_mentions.inspect
|
12
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require "#{File.dirname(__FILE__)}/../dataflow"
|
2
|
+
|
3
|
+
class AnimalHouse
|
4
|
+
include Dataflow
|
5
|
+
declare :small_cat, :big_cat
|
6
|
+
|
7
|
+
def fetch_big_cat
|
8
|
+
Thread.new { unify big_cat, small_cat.upcase }
|
9
|
+
unify small_cat, 'cat'
|
10
|
+
big_cat
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
puts AnimalHouse.new.fetch_big_cat
|
15
|
+
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require "#{File.dirname(__FILE__)}/../dataflow"
|
2
|
+
include Dataflow
|
3
|
+
|
4
|
+
Ping = Actor.new {
|
5
|
+
3.times {
|
6
|
+
case receive
|
7
|
+
when "Ping"
|
8
|
+
puts "Ping"
|
9
|
+
Pong.send "Pong"
|
10
|
+
end
|
11
|
+
}
|
12
|
+
}
|
13
|
+
|
14
|
+
Pong = Actor.new {
|
15
|
+
3.times {
|
16
|
+
case receive
|
17
|
+
when "Pong"
|
18
|
+
puts "Pong"
|
19
|
+
Ping.send "Ping"
|
20
|
+
end
|
21
|
+
}
|
22
|
+
}
|
23
|
+
|
24
|
+
Ping.send "Ping"
|
25
|
+
Ping.join
|
26
|
+
Pong.join
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require "#{File.dirname(__FILE__)}/../dataflow"
|
2
|
+
require 'net/http'
|
3
|
+
include Dataflow
|
4
|
+
|
5
|
+
# Be gentle running this one
|
6
|
+
Thread.abort_on_exception = true
|
7
|
+
local do |port, stream, branding_occurences, number_of_mentions|
|
8
|
+
unify port, Dataflow::Port.new(stream)
|
9
|
+
10.times { Thread.new(port) {|p| p.send Net::HTTP.get_response(URI.parse("http://www.cuil.com/search?q=#{rand(1000)}")).body } }
|
10
|
+
Thread.new { unify branding_occurences, stream.take(10).map {|http_body| http_body.scan /cuil/ } }
|
11
|
+
Thread.new { unify number_of_mentions, branding_occurences.map {|occurences| occurences.length } }
|
12
|
+
puts number_of_mentions.inspect
|
13
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
require "#{File.dirname(__FILE__)}/../dataflow"
|
2
|
+
include Dataflow
|
3
|
+
|
4
|
+
local do |port, stream|
|
5
|
+
unify port, Dataflow::Port.new(stream)
|
6
|
+
Thread.new {port.send 2}
|
7
|
+
Thread.new {port.send 8}
|
8
|
+
Thread.new {port.send 1024}
|
9
|
+
puts stream.take(3).sort.inspect
|
10
|
+
end
|
data/examples/ring.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require "#{File.dirname(__FILE__)}/../dataflow"
|
2
|
+
include Dataflow
|
3
|
+
|
4
|
+
# Send M messages each along a ring of N nodes
|
5
|
+
N = 4
|
6
|
+
M = 2
|
7
|
+
actors = Array.new(N) { Dataflow::Variable.new }
|
8
|
+
|
9
|
+
N.times do |n|
|
10
|
+
unify actors[n], Actor.new {
|
11
|
+
M.times do |m|
|
12
|
+
receive
|
13
|
+
puts "[#{n} #{m}]"
|
14
|
+
actors[(n+1) % N].send :msg
|
15
|
+
end
|
16
|
+
puts "[#{n}] done"
|
17
|
+
}
|
18
|
+
end
|
19
|
+
|
20
|
+
actors.first.send :msg
|
21
|
+
actors.each { |x| x.join }
|
data/lib/actor.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
module Dataflow
|
2
|
+
class Actor < Thread
|
3
|
+
def initialize(&block)
|
4
|
+
@stream = Variable.new
|
5
|
+
@port = Port.new(@stream)
|
6
|
+
# Run this block in a new thread
|
7
|
+
super { instance_eval &block }
|
8
|
+
end
|
9
|
+
|
10
|
+
def send message
|
11
|
+
@port.send message
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def receive
|
17
|
+
result = @stream.head
|
18
|
+
@stream = @stream.tail
|
19
|
+
result
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
data/lib/port.rb
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'thread'
|
2
|
+
|
3
|
+
module Dataflow
|
4
|
+
class Port
|
5
|
+
include Dataflow
|
6
|
+
LOCK = Mutex.new
|
7
|
+
|
8
|
+
class Stream
|
9
|
+
include Dataflow
|
10
|
+
declare :head, :tail
|
11
|
+
|
12
|
+
# Defining each allows us to use the enumerable mixin
|
13
|
+
# None of the list can be garbage collected less the head is
|
14
|
+
# garbage collected, so it will grow indefinitely even though
|
15
|
+
# the function isn't recursive.
|
16
|
+
include Enumerable
|
17
|
+
def each
|
18
|
+
s = self
|
19
|
+
loop do
|
20
|
+
yield s.head
|
21
|
+
s = s.tail
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Backported Enumerable#take for any 1.8.6 compatible Ruby
|
26
|
+
unless method_defined?(:take)
|
27
|
+
def take(num)
|
28
|
+
result = []
|
29
|
+
each_with_index do |x, i|
|
30
|
+
return result if num == i
|
31
|
+
result << x
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Create a stream object, bind it to the input variable
|
38
|
+
# Instance variables are necessary because @end is state
|
39
|
+
def initialize(x)
|
40
|
+
@end = Stream.new
|
41
|
+
unify x, @end
|
42
|
+
end
|
43
|
+
|
44
|
+
# This needs to be synchronized because it uses @end as state
|
45
|
+
def send value
|
46
|
+
LOCK.synchronize do
|
47
|
+
unify @end.head, value
|
48
|
+
unify @end.tail, Stream.new
|
49
|
+
@end = @end.tail
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
data/spec/actor_spec.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
require "#{File.dirname(__FILE__)}/spec_helper"
|
2
|
+
Thread.abort_on_exception = true
|
3
|
+
|
4
|
+
describe 'Syncronously sending to an Actor' do
|
5
|
+
it 'passes in each message received and preserves order' do
|
6
|
+
local do |port, stream, actor|
|
7
|
+
unify port, Dataflow::Port.new(stream)
|
8
|
+
unify actor, Dataflow::Actor.new { 3.times { port.send receive } }
|
9
|
+
actor.send 1
|
10
|
+
actor.send 2
|
11
|
+
stream.take(2).should == [1, 2]
|
12
|
+
actor.send 3
|
13
|
+
stream.take(3).should == [1, 2, 3]
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
describe 'Asyncronously sending to an Actor' do
|
19
|
+
it 'passes in each message received and preserves order' do
|
20
|
+
local do |port, stream, actor|
|
21
|
+
unify port, Dataflow::Port.new(stream)
|
22
|
+
unify actor, Dataflow::Actor.new { 3.times { port.send receive } }
|
23
|
+
Thread.new {actor.send 2}
|
24
|
+
Thread.new {actor.send 8}
|
25
|
+
Thread.new {actor.send 1024}
|
26
|
+
stream.take(3).sort.should == [2, 8, 1024]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require "#{File.dirname(__FILE__)}/spec_helper"
|
2
|
+
|
3
|
+
describe 'A by_need trigger' do
|
4
|
+
it 'creates a need when a method is called on it' do
|
5
|
+
local do |x, y, z|
|
6
|
+
Thread.new { unify y, by_need { 4 } }
|
7
|
+
Thread.new { unify z, x + y }
|
8
|
+
Thread.new { unify x, by_need { 3 } }
|
9
|
+
z.should == 7
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
require "#{File.dirname(__FILE__)}/spec_helper"
|
2
|
+
|
3
|
+
context 'Using "local" for local variables' do
|
4
|
+
describe 'An unbound Variable' do
|
5
|
+
it 'suspends if an unbound variable has a method called on it until it is bound' do
|
6
|
+
local do |big_cat, small_cat|
|
7
|
+
Thread.new { unify big_cat, small_cat.upcase }
|
8
|
+
unify small_cat, 'cat'
|
9
|
+
big_cat.should == 'CAT'
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'suspends if an unbound variable has a method called on it until it is bound with nil' do
|
14
|
+
local do |is_nil, var_nil|
|
15
|
+
Thread.new { unify is_nil, var_nil.nil? }
|
16
|
+
unify var_nil, nil
|
17
|
+
is_nil.should be_true
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'suspends if an unbound variable has a method called on it until it is bound (with nested local variables)' do
|
22
|
+
local do |small_cat|
|
23
|
+
local do |big_cat|
|
24
|
+
Thread.new { unify big_cat, small_cat.upcase }
|
25
|
+
unify small_cat, 'cat'
|
26
|
+
big_cat.should == 'CAT'
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'performs order-determining concurrency' do
|
32
|
+
local do |x, y, z|
|
33
|
+
Thread.new { unify y, x + 2 }
|
34
|
+
Thread.new { unify z, y + 3 }
|
35
|
+
Thread.new { unify x, 1 }
|
36
|
+
z.should == 6
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'binds on unification' do
|
41
|
+
local do |animal|
|
42
|
+
unify animal, 'cat'
|
43
|
+
animal.should == 'cat'
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
describe 'A bound Variable' do
|
50
|
+
it 'does not complain when unifying with an equal object' do
|
51
|
+
lambda do
|
52
|
+
local do |animal|
|
53
|
+
unify animal, 'cat'
|
54
|
+
unify animal, 'cat'
|
55
|
+
end
|
56
|
+
end.should_not raise_error
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'does not complain when unifying with an unequal object when shadowing' do
|
60
|
+
lambda do
|
61
|
+
local do |animal|
|
62
|
+
unify animal, 'cat'
|
63
|
+
local do |animal|
|
64
|
+
unify animal, 'dog'
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end.should_not raise_error
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'complains when unifying with an unequal object' do
|
71
|
+
lambda do
|
72
|
+
local do |animal|
|
73
|
+
unify animal, 'cat'
|
74
|
+
unify animal, 'dog'
|
75
|
+
end
|
76
|
+
end.should raise_error(Dataflow::UnificationError)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
context 'Using "declare" for object-specific read-only attributes' do
|
81
|
+
class Store
|
82
|
+
include Dataflow
|
83
|
+
declare :animal, :big_cat, :small_cat, :x, :y, :z, :is_nil, :var_nil
|
84
|
+
end
|
85
|
+
before { @store = Store.new }
|
86
|
+
|
87
|
+
describe 'An unbound Variable' do
|
88
|
+
it 'suspends if an unbound variable has a method called on it until it is bound' do
|
89
|
+
Thread.new { unify @store.big_cat, @store.small_cat.upcase }
|
90
|
+
unify @store.small_cat, 'cat'
|
91
|
+
@store.big_cat.should == 'CAT'
|
92
|
+
end
|
93
|
+
|
94
|
+
it 'suspends if an unbound variable has a method called on it until it is bound with nil' do
|
95
|
+
Thread.new { unify @store.is_nil, @store.var_nil.nil? }
|
96
|
+
unify @store.var_nil, nil
|
97
|
+
@store.is_nil.should be_true
|
98
|
+
end
|
99
|
+
|
100
|
+
it 'performs order-determining concurrency' do
|
101
|
+
Thread.new { unify @store.y, @store.x + 2 }
|
102
|
+
Thread.new { unify @store.z, @store.y + 3 }
|
103
|
+
Thread.new { unify @store.x, 1 }
|
104
|
+
@store.z.should == 6
|
105
|
+
end
|
106
|
+
|
107
|
+
it 'binds on unification' do
|
108
|
+
unify @store.animal, 'cat'
|
109
|
+
@store.animal.should == 'cat'
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
describe 'A bound Variable' do
|
114
|
+
it 'does not complain when unifying with an equal object' do
|
115
|
+
lambda do
|
116
|
+
unify @store.animal, 'cat'
|
117
|
+
unify @store.animal, 'cat'
|
118
|
+
end.should_not raise_error
|
119
|
+
end
|
120
|
+
|
121
|
+
it 'complains when unifying with an unequal object' do
|
122
|
+
lambda do
|
123
|
+
unify @store.animal, 'cat'
|
124
|
+
unify @store.animal, 'dog'
|
125
|
+
end.should raise_error(Dataflow::UnificationError)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require "#{File.dirname(__FILE__)}/spec_helper"
|
2
|
+
|
3
|
+
describe 'A need_later expression' do
|
4
|
+
it 'returns a future to be calculated later' do
|
5
|
+
local do |x, y, z|
|
6
|
+
unify y, need_later { 4 }
|
7
|
+
unify z, need_later { x + y }
|
8
|
+
unify x, need_later { 3 }
|
9
|
+
z.should == 7
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
data/spec/port_spec.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
require "#{File.dirname(__FILE__)}/spec_helper"
|
2
|
+
|
3
|
+
describe 'Syncronously sending to a Port' do
|
4
|
+
it 'extends the length of the stream and preserves order' do
|
5
|
+
local do |port, stream|
|
6
|
+
unify port, Dataflow::Port.new(stream)
|
7
|
+
port.send 1
|
8
|
+
port.send 2
|
9
|
+
stream.take(2).should == [1, 2]
|
10
|
+
port.send 3
|
11
|
+
stream.take(3).should == [1, 2, 3]
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
describe 'Asyncronously sending to a Port' do
|
17
|
+
it 'extends the length of the stream and does not preserve order' do
|
18
|
+
local do |port, stream|
|
19
|
+
unify port, Dataflow::Port.new(stream)
|
20
|
+
Thread.new {port.send 2}
|
21
|
+
Thread.new {port.send 8}
|
22
|
+
Thread.new {port.send 1024}
|
23
|
+
stream.take(3).sort.should == [2, 8, 1024]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/spec/spec.opts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: dataflow
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Larry Diehl
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-06-13 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: Dataflow concurrency for Ruby (inspired by the Oz language)
|
17
|
+
email: larrytheliquid@gmail.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files: []
|
23
|
+
|
24
|
+
files:
|
25
|
+
- LICENSE
|
26
|
+
- History.txt
|
27
|
+
- Rakefile
|
28
|
+
- README.textile
|
29
|
+
- dataflow.rb
|
30
|
+
- lib/actor.rb
|
31
|
+
- lib/port.rb
|
32
|
+
- examples/local_variables.rb
|
33
|
+
- examples/dataflow_http_gets.rb
|
34
|
+
- examples/instance_variables.rb
|
35
|
+
- examples/data_driven.rb
|
36
|
+
- examples/ring.rb
|
37
|
+
- examples/port_send.rb
|
38
|
+
- examples/laziness.rb
|
39
|
+
- examples/port_http_gets.rb
|
40
|
+
- examples/future_http_gets.rb
|
41
|
+
- examples/messages.rb
|
42
|
+
has_rdoc: false
|
43
|
+
homepage: http://github.com/larrytheliquid/dataflow
|
44
|
+
post_install_message:
|
45
|
+
rdoc_options: []
|
46
|
+
|
47
|
+
require_paths:
|
48
|
+
- lib
|
49
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: "0"
|
54
|
+
version:
|
55
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - ">="
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: "0"
|
60
|
+
version:
|
61
|
+
requirements: []
|
62
|
+
|
63
|
+
rubyforge_project: dataflow
|
64
|
+
rubygems_version: 1.3.1
|
65
|
+
signing_key:
|
66
|
+
specification_version: 2
|
67
|
+
summary: Dataflow concurrency for Ruby (inspired by the Oz language)
|
68
|
+
test_files:
|
69
|
+
- spec/need_later_spec.rb
|
70
|
+
- spec/by_need_spec.rb
|
71
|
+
- spec/spec_helper.rb
|
72
|
+
- spec/port_spec.rb
|
73
|
+
- spec/spec.opts
|
74
|
+
- spec/dataflow_spec.rb
|
75
|
+
- spec/actor_spec.rb
|