functional-ruby 0.5.0 → 0.6.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.
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