functional-ruby 0.5.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +154 -562
  3. data/lib/functional/agent.rb +130 -0
  4. data/lib/functional/all.rb +9 -1
  5. data/lib/functional/behavior.rb +72 -39
  6. data/lib/functional/cached_thread_pool.rb +122 -0
  7. data/lib/functional/concurrency.rb +32 -24
  8. data/lib/functional/core.rb +2 -62
  9. data/lib/functional/event.rb +53 -0
  10. data/lib/functional/event_machine_defer_proxy.rb +23 -0
  11. data/lib/functional/fixed_thread_pool.rb +89 -0
  12. data/lib/functional/future.rb +42 -0
  13. data/lib/functional/global_thread_pool.rb +3 -0
  14. data/lib/functional/obligation.rb +121 -0
  15. data/lib/functional/promise.rb +194 -0
  16. data/lib/functional/thread_pool.rb +61 -0
  17. data/lib/functional/utilities.rb +114 -0
  18. data/lib/functional/version.rb +1 -1
  19. data/lib/functional.rb +1 -0
  20. data/lib/functional_ruby.rb +1 -0
  21. data/md/behavior.md +147 -0
  22. data/md/concurrency.md +465 -0
  23. data/md/future.md +32 -0
  24. data/md/obligation.md +32 -0
  25. data/md/pattern_matching.md +512 -0
  26. data/md/promise.md +220 -0
  27. data/md/utilities.md +53 -0
  28. data/spec/functional/agent_spec.rb +405 -0
  29. data/spec/functional/behavior_spec.rb +12 -33
  30. data/spec/functional/cached_thread_pool_spec.rb +112 -0
  31. data/spec/functional/concurrency_spec.rb +55 -0
  32. data/spec/functional/event_machine_defer_proxy_spec.rb +246 -0
  33. data/spec/functional/event_spec.rb +114 -0
  34. data/spec/functional/fixed_thread_pool_spec.rb +84 -0
  35. data/spec/functional/future_spec.rb +115 -0
  36. data/spec/functional/obligation_shared.rb +121 -0
  37. data/spec/functional/pattern_matching_spec.rb +10 -8
  38. data/spec/functional/promise_spec.rb +310 -0
  39. data/spec/functional/thread_pool_shared.rb +209 -0
  40. data/spec/functional/utilities_spec.rb +149 -0
  41. data/spec/spec_helper.rb +2 -0
  42. metadata +55 -5
@@ -0,0 +1,130 @@
1
+ require 'observer'
2
+ require 'thread'
3
+
4
+ require 'functional/global_thread_pool'
5
+
6
+ module Functional
7
+
8
+ # An agent is a single atomic value that represents an identity. The current value
9
+ # of the agent can be requested at any time (#deref). Each agent has a work queue and operates on
10
+ # the global thread pool. Consumers can #post code blocks to the agent. The code block (function)
11
+ # will receive the current value of the agent as its sole parameter. The return value of the block
12
+ # will become the new value of the agent. Agents support two error handling modes: fail and continue.
13
+ # A good example of an agent is a shared incrementing counter, such as the score in a video game.
14
+ class Agent
15
+ include Observable
16
+
17
+ TIMEOUT = 5
18
+
19
+ attr_reader :initial
20
+ attr_reader :timeout
21
+
22
+ def initialize(initial, timeout = TIMEOUT)
23
+ @value = initial
24
+ @timeout = timeout
25
+ @rescuers = []
26
+ @validator = nil
27
+ @queue = Queue.new
28
+
29
+ $GLOBAL_THREAD_POOL << proc{ work }
30
+ end
31
+
32
+ def value(timeout = 0) return @value; end
33
+ alias_method :deref, :value
34
+
35
+ def rescue(clazz = Exception, &block)
36
+ @rescuers << Rescuer.new(clazz, block) if block_given?
37
+ return self
38
+ end
39
+ alias_method :catch, :rescue
40
+ alias_method :on_error, :rescue
41
+
42
+ def validate(&block)
43
+ @validator = block if block_given?
44
+ return self
45
+ end
46
+ alias_method :validates, :validate
47
+ alias_method :validate_with, :validate
48
+ alias_method :validates_with, :validate
49
+
50
+ def post(&block)
51
+ return @queue.length unless block_given?
52
+ @queue << block
53
+ return @queue.length
54
+ end
55
+
56
+ def <<(block)
57
+ self.post(&block)
58
+ return self
59
+ end
60
+
61
+ def length
62
+ @queue.length
63
+ end
64
+ alias_method :size, :length
65
+ alias_method :count, :length
66
+
67
+ alias_method :add_watch, :add_observer
68
+
69
+ private
70
+
71
+ # @private
72
+ Rescuer = Struct.new(:clazz, :block)
73
+
74
+ # @private
75
+ def try_rescue(ex) # :nodoc:
76
+ rescuer = @rescuers.find{|r| ex.is_a?(r.clazz) }
77
+ rescuer.block.call(ex) if rescuer
78
+ rescue Exception => e
79
+ # supress
80
+ end
81
+
82
+ # @private
83
+ def work # :nodoc:
84
+ loop do
85
+ Thread.pass
86
+ handler = @queue.pop
87
+ begin
88
+ result = Timeout.timeout(@timeout){
89
+ handler.call(@value)
90
+ }
91
+ if @validator.nil? || @validator.call(result)
92
+ @value = result
93
+ changed
94
+ notify_observers(Time.now, @value)
95
+ end
96
+ rescue Exception => ex
97
+ try_rescue(ex)
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
103
+
104
+ module Kernel
105
+
106
+ def agent(initial, timeout = Functional::Agent::TIMEOUT)
107
+ return Functional::Agent.new(initial, timeout)
108
+ end
109
+ module_function :agent
110
+
111
+ def deref(agent, timeout = nil)
112
+ if agent.respond_to?(:deref)
113
+ return agent.deref(timeout)
114
+ elsif agent.respond_to?(:value)
115
+ return agent.deref(timeout)
116
+ else
117
+ return nil
118
+ end
119
+ end
120
+ module_function :deref
121
+
122
+ def post(agent, &block)
123
+ if agent.respond_to?(:post)
124
+ return agent.post(&block)
125
+ else
126
+ return nil
127
+ end
128
+ end
129
+ module_function :deref
130
+ end
@@ -1,5 +1,13 @@
1
+ require 'functional/agent'
1
2
  require 'functional/behavior'
3
+ require 'functional/cached_thread_pool'
2
4
  require 'functional/concurrency'
3
- require 'functional/core'
5
+ require 'functional/event'
6
+ require 'functional/fixed_thread_pool'
7
+ require 'functional/future'
8
+ require 'functional/obligation'
4
9
  require 'functional/pattern_matching'
10
+ require 'functional/promise'
11
+ require 'functional/thread_pool'
12
+ require 'functional/utilities'
5
13
  require 'functional/version'
@@ -1,12 +1,78 @@
1
- def behavior_info(name, callbacks = {})
2
- $__behavior_info__ ||= {}
3
- $__behavior_info__[name.to_sym] = callbacks.inject({}){|memo,(k,v)| memo[k.to_sym] = v; memo}
4
- end
1
+ module Kernel
2
+
3
+ BehaviorError = Class.new(StandardError)
4
+
5
+ # Define a behavioral specification (interface).
6
+ #
7
+ # @param name [Symbol] the name of the behavior
8
+ # @param functions [Hash] function names and their arity as key/value pairs
9
+ def behavior_info(name, functions = {})
10
+ $__behavior_info__ ||= {}
11
+ $__behavior_info__[name.to_sym] = functions.inject({}){|memo,(k,v)| memo[k.to_sym] = v; memo}
12
+ end
13
+
14
+ alias :behaviour_info :behavior_info
15
+ alias :interface :behavior_info
16
+
17
+ module_function :behavior_info
18
+ module_function :behaviour_info
19
+ module_function :interface
20
+
21
+ # Specify a #behavior_info to enforce on the enclosing class
22
+ #
23
+ # @param name [Symbol] name of the #behavior_info being implemented
24
+ def behavior(name)
25
+
26
+ name = name.to_sym
27
+ raise BehaviorError.new("undefined behavior '#{name}'") if $__behavior_info__[name].nil?
28
+
29
+ clazz = self.method(:behavior).receiver
30
+
31
+ unless clazz.instance_methods(false).include?(:behaviors)
32
+ class << clazz
33
+ def behaviors
34
+ @behaviors ||= []
35
+ end
36
+ end
37
+ end
38
+
39
+ clazz.behaviors << name
40
+
41
+ class << clazz
42
+ def new(*args, &block)
43
+ name = self.behaviors.first
44
+ obj = super
45
+ unless obj.behaves_as?(name)
46
+ raise BehaviorError.new("undefined callback functions in #{self} (behavior '#{name}')")
47
+ else
48
+ return obj
49
+ end
50
+ end
51
+ end
52
+ end
5
53
 
6
- alias :behaviour_info :behavior_info
7
- alias :interface :behavior_info
54
+ alias :behaviour :behavior
55
+ alias :behaves_as :behavior
56
+
57
+ module_function :behavior
58
+ module_function :behaviour
59
+ module_function :behaves_as
60
+ end
8
61
 
9
62
  class Object
63
+
64
+ # Does the object implement the given #behavior_info?
65
+ #
66
+ # @note Will return true if the object implements the
67
+ # required methods. The object's class hierarchy does
68
+ # not necessarily have to include a corresponding
69
+ # #behavior call.
70
+ #
71
+ # @param name [Symbol] name of the #behavior_info to
72
+ # verify behavior against.
73
+ #
74
+ # @return [Boolean] whether or not the required public
75
+ # methods are implemented
10
76
  def behaves_as?(name)
11
77
 
12
78
  name = name.to_sym
@@ -24,36 +90,3 @@ class Object
24
90
  return true
25
91
  end
26
92
  end
27
-
28
- def behavior(name)
29
-
30
- name = name.to_sym
31
- raise ArgumentError.new("undefined behavior '#{name}'") if $__behavior_info__[name].nil?
32
-
33
- clazz = self.method(:behavior).receiver
34
-
35
- unless clazz.instance_methods(false).include?(:behaviors)
36
- class << clazz
37
- def behaviors
38
- @behaviors ||= []
39
- end
40
- end
41
- end
42
-
43
- clazz.behaviors << name
44
-
45
- class << clazz
46
- def new(*args, &block)
47
- name = self.behaviors.first
48
- obj = super
49
- unless obj.behaves_as?(name)
50
- raise ArgumentError.new("undefined callback functions in #{self} (behavior '#{name}')")
51
- else
52
- return obj
53
- end
54
- end
55
- end
56
- end
57
-
58
- alias :behaviour :behavior
59
- alias :behaves_as :behavior
@@ -0,0 +1,122 @@
1
+ require 'thread'
2
+
3
+ require 'functional/thread_pool'
4
+ require 'functional/utilities'
5
+
6
+ module Functional
7
+
8
+ def self.new_cached_thread_pool
9
+ return CachedThreadPool.new
10
+ end
11
+
12
+ class CachedThreadPool < ThreadPool
13
+ behavior(:thread_pool)
14
+
15
+ DEFAULT_GC_INTERVAL = 60
16
+ DEFAULT_THREAD_IDLETIME = 60
17
+
18
+ attr_reader :working
19
+
20
+ def initialize(opts = {})
21
+ @gc_interval = opts[:gc_interval] || DEFAULT_GC_INTERVAL
22
+ @thread_idletime = opts[:thread_idletime] || DEFAULT_THREAD_IDLETIME
23
+ super()
24
+ @working = 0
25
+ @mutex = Mutex.new
26
+ end
27
+
28
+ def kill
29
+ @status = :killed
30
+ @mutex.synchronize do
31
+ @pool.each{|t| Thread.kill(t.thread) }
32
+ end
33
+ end
34
+
35
+ def size
36
+ return @pool.length
37
+ end
38
+
39
+ def post(*args, &block)
40
+ raise ArgumentError.new('no block given') unless block_given?
41
+ if running?
42
+ collect_garbage if @pool.empty?
43
+ @mutex.synchronize do
44
+ if @working >= @pool.length
45
+ create_worker_thread
46
+ end
47
+ @queue << [args, block]
48
+ end
49
+ return true
50
+ else
51
+ return false
52
+ end
53
+ end
54
+
55
+ # @private
56
+ def status # :nodoc:
57
+ @mutex.synchronize do
58
+ @pool.collect do |worker|
59
+ [
60
+ worker.status,
61
+ worker.status == :idle ? delta(worker.idletime, timestamp) : nil,
62
+ worker.thread.status
63
+ ]
64
+ end
65
+ end
66
+ end
67
+
68
+ private
69
+
70
+ Worker = Struct.new(:status, :idletime, :thread)
71
+
72
+ # @private
73
+ def create_worker_thread # :nodoc:
74
+ worker = Worker.new(:idle, timestamp, nil)
75
+
76
+ worker.thread = Thread.new(worker) do |me|
77
+
78
+ loop do
79
+ task = @queue.pop
80
+
81
+ @working += 1
82
+ me.status = :working
83
+
84
+ if task == :stop
85
+ me.status = :stopping
86
+ break
87
+ else
88
+ task.last.call(*task.first)
89
+ @working -= 1
90
+ me.status = :idle
91
+ me.idletime = timestamp
92
+ end
93
+ end
94
+
95
+ @pool.delete(me)
96
+ if @pool.empty?
97
+ @termination.set
98
+ @status = :shutdown unless killed?
99
+ end
100
+ end
101
+
102
+ @pool << worker
103
+ end
104
+
105
+ # @private
106
+ def collect_garbage # :nodoc:
107
+ @collector = Thread.new do
108
+ loop do
109
+ sleep(@gc_interval)
110
+ @mutex.synchronize do
111
+ @pool.reject! do |worker|
112
+ worker.thread.status.nil? ||
113
+ (worker.status == :idle && @thread_idletime >= delta(worker.idletime, timestamp))
114
+ end
115
+ end
116
+ @working = @pool.count{|worker| worker.status == :working}
117
+ break if @pool.empty?
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
@@ -1,27 +1,35 @@
1
- # http://docs.oracle.com/javase/tutorial/java/javaOO/enum.html
2
- # http://www.lesismore.co.za/rubyenums.html
3
- # http://gistflow.com/posts/682-ruby-enums-approaches
1
+ require 'thread'
4
2
 
5
- # http://richhickey.github.io/clojure/clojure.core-api.html
6
- # * agent
7
- # * add-watch
8
- # * apply
9
- # * assert
10
- # * await
11
- # * future
12
- # * memoize
13
- # * promise
14
- # * send
15
- # * slurp
3
+ require 'functional/agent'
4
+ require 'functional/future'
5
+ require 'functional/promise'
16
6
 
17
- # Other stuff
18
- # * pure - creates an object, freezes it, and removes the unfreeze method
19
- # * retry - retries something x times if it fails
20
- # * promise = ala JavaScript http://blog.parse.com/2013/01/29/whats-so-great-about-javascript-promises/
21
- # * ada range type - http://en.wikibooks.org/wiki/Ada_Programming/Types/range
22
- # * slurpee - slurp + erb parsing
23
- # * spawn/send/receive - http://www.erlang.org/doc/reference_manual/processes.html
7
+ require 'functional/thread_pool'
8
+ require 'functional/cached_thread_pool'
9
+ require 'functional/fixed_thread_pool'
24
10
 
25
- # Using Ruby's queue for sending messages between threads
26
- # * http://www.subelsky.com/2010/02/using-rubys-queue-class-to-manage-inter.html
27
- # * http://www.ruby-doc.org/stdlib-1.9.3/libdoc/thread/rdoc/Queue.html#method-i-pop
11
+ require 'functional/global_thread_pool'
12
+
13
+ require 'functional/event_machine_defer_proxy' if defined?(EventMachine)
14
+
15
+ module Kernel
16
+
17
+ # Post the given agruments and block to the Global Thread Pool.
18
+ #
19
+ # @param args [Array] zero or more arguments for the block
20
+ # @param block [Proc] operation to be performed concurrently
21
+ #
22
+ # @return [true,false] success/failre of thread creation
23
+ #
24
+ # @note Althought based on Go's goroutines and Erlang's spawn/1,
25
+ # Ruby has a vastly different runtime. Threads aren't nearly as
26
+ # efficient in Ruby. Use this function appropriately.
27
+ #
28
+ # @see http://golang.org/doc/effective_go.html#goroutines
29
+ # @see https://gobyexample.com/goroutines
30
+ def go(*args, &block)
31
+ return false unless block_given?
32
+ $GLOBAL_THREAD_POOL.post(*args, &block)
33
+ end
34
+ module_function :go
35
+ end
@@ -1,62 +1,2 @@
1
- require 'pp'
2
- require 'stringio'
3
-
4
- Infinity = 1/0.0 unless defined?(Infinity)
5
- NaN = 0/0.0 unless defined?(NaN)
6
-
7
- module Kernel
8
-
9
- private
10
-
11
- def repl?
12
- return ($0 == 'irb' || $0 == 'pry' || $0 == 'script/rails' || !!($0 =~ /bin\/bundle$/))
13
- end
14
- module_function :repl?
15
-
16
- def safe(*args, &block)
17
- raise ArgumentError.new('no block given') unless block_given?
18
- result = nil
19
- t = Thread.new do
20
- $SAFE = 3
21
- result = self.instance_exec(*args, &block)
22
- end
23
- t.join
24
- return result
25
- end
26
- module_function :safe
27
-
28
- # http://rhaseventh.blogspot.com/2008/07/ruby-and-rails-how-to-get-pp-pretty.html
29
- def pp_s(*objs)
30
- s = StringIO.new
31
- objs.each {|obj|
32
- PP.pp(obj, s)
33
- }
34
- s.rewind
35
- s.read
36
- end
37
- module_function :pp_s
38
-
39
- # Compute the difference (delta) between two values.
40
- #
41
- # When a block is given the block will be applied to both arguments.
42
- # Using a block in this way allows computation against a specific field
43
- # in a data set of hashes or objects.
44
- #
45
- # @yield iterates over each element in the data set
46
- # @yieldparam item each element in the data set
47
- #
48
- # @param [Object] v1 the first value
49
- # @param [Object] v2 the second value
50
- #
51
- # @return [Float] positive value representing the difference
52
- # between the two parameters
53
- def delta(v1, v2)
54
- if block_given?
55
- v1 = yield(v1)
56
- v2 = yield(v2)
57
- end
58
- return (v1 - v2).abs
59
- end
60
- module_function :delta
61
-
62
- end
1
+ require 'functional/behavior'
2
+ require 'functional/pattern_matching'
@@ -0,0 +1,53 @@
1
+ require 'thread'
2
+ require 'timeout'
3
+
4
+ module Functional
5
+
6
+ class Event
7
+
8
+ def initialize
9
+ @set = false
10
+ @notifier = Queue.new
11
+ @mutex = Mutex.new
12
+ @waiting = 0
13
+ end
14
+
15
+ def set?
16
+ return @set == true
17
+ end
18
+
19
+ def set
20
+ return true if set?
21
+ @mutex.synchronize {
22
+ @set = true
23
+ while @waiting > 0
24
+ @notifier << :set
25
+ @waiting -= 1
26
+ end
27
+ }
28
+ end
29
+
30
+ def reset
31
+ @mutex.synchronize {
32
+ @set = false
33
+ }
34
+ end
35
+
36
+ def wait(timeout = nil)
37
+ return true if set?
38
+
39
+ if timeout.nil?
40
+ @waiting += 1
41
+ @notifier.pop
42
+ else
43
+ Timeout::timeout(timeout) do
44
+ @waiting += 1
45
+ @notifier.pop
46
+ end
47
+ end
48
+ return true
49
+ rescue Timeout::Error
50
+ return false
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,23 @@
1
+ require 'functional/global_thread_pool'
2
+
3
+ module Functional
4
+
5
+ class EventMachineDeferProxy
6
+ behavior(:global_thread_pool)
7
+
8
+ def post(*args, &block)
9
+ if args.empty?
10
+ EventMachine.defer(block)
11
+ else
12
+ new_block = proc{ block.call(*args) }
13
+ EventMachine.defer(new_block)
14
+ end
15
+ return true
16
+ end
17
+
18
+ def <<(block)
19
+ EventMachine.defer(block)
20
+ return self
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,89 @@
1
+ require 'thread'
2
+
3
+ require 'functional/thread_pool'
4
+ require 'functional/event'
5
+
6
+ module Functional
7
+
8
+ def self.new_fixed_thread_pool(size)
9
+ return FixedThreadPool.new(size)
10
+ end
11
+
12
+ class FixedThreadPool < ThreadPool
13
+ behavior(:thread_pool)
14
+
15
+ MIN_POOL_SIZE = 1
16
+ MAX_POOL_SIZE = 1024
17
+
18
+ def initialize(size)
19
+ super()
20
+ if size < MIN_POOL_SIZE || size > MAX_POOL_SIZE
21
+ raise ArgumentError.new("size must be between #{MIN_POOL_SIZE} and #{MAX_POOL_SIZE}")
22
+ end
23
+
24
+ @pool = size.times.collect{ create_worker_thread }
25
+ collect_garbage
26
+ end
27
+
28
+ def kill
29
+ @status = :killed
30
+ @pool.each{|t| Thread.kill(t) }
31
+ end
32
+
33
+ def size
34
+ if running?
35
+ return @pool.length
36
+ else
37
+ return 0
38
+ end
39
+ end
40
+
41
+ def post(*args, &block)
42
+ raise ArgumentError.new('no block given') unless block_given?
43
+ if running?
44
+ @queue << [args, block]
45
+ return true
46
+ else
47
+ return false
48
+ end
49
+ end
50
+
51
+ # @private
52
+ def status # :nodoc:
53
+ @pool.collect{|t| t.status }
54
+ end
55
+
56
+ private
57
+
58
+ # @private
59
+ def create_worker_thread # :nodoc:
60
+ Thread.new do
61
+ loop do
62
+ task = @queue.pop
63
+ if task == :stop
64
+ break
65
+ else
66
+ task.last.call(*task.first)
67
+ end
68
+ end
69
+ @pool.delete(Thread.current)
70
+ if @pool.empty?
71
+ @termination.set
72
+ @status = :shutdown unless killed?
73
+ end
74
+ end
75
+ end
76
+
77
+ # @private
78
+ def collect_garbage # :nodoc:
79
+ @collector = Thread.new do
80
+ sleep(1)
81
+ @pool.size.times do |i|
82
+ if @pool[i].status.nil?
83
+ @pool[i] = create_worker_thread
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end