functional-ruby 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +4 -0
  3. data/README.md +14 -12
  4. data/doc/memo.md +192 -0
  5. data/doc/pattern_matching.md +481 -0
  6. data/doc/protocol.md +219 -0
  7. data/doc/record.md +247 -0
  8. data/lib/functional/abstract_struct.rb +8 -8
  9. data/lib/functional/delay.rb +31 -38
  10. data/lib/functional/either.rb +48 -45
  11. data/lib/functional/final_struct.rb +23 -34
  12. data/lib/functional/final_var.rb +20 -21
  13. data/lib/functional/memo.rb +33 -24
  14. data/lib/functional/method_signature.rb +1 -2
  15. data/lib/functional/option.rb +7 -7
  16. data/lib/functional/pattern_matching.rb +12 -10
  17. data/lib/functional/protocol.rb +2 -4
  18. data/lib/functional/protocol_info.rb +5 -3
  19. data/lib/functional/record.rb +82 -16
  20. data/lib/functional/synchronization.rb +88 -0
  21. data/lib/functional/tuple.rb +14 -4
  22. data/lib/functional/type_check.rb +0 -2
  23. data/lib/functional/union.rb +5 -4
  24. data/lib/functional/value_struct.rb +5 -3
  25. data/lib/functional/version.rb +1 -1
  26. data/spec/functional/complex_pattern_matching_spec.rb +1 -2
  27. data/spec/functional/configuration_spec.rb +0 -2
  28. data/spec/functional/delay_spec.rb +0 -2
  29. data/spec/functional/either_spec.rb +0 -1
  30. data/spec/functional/final_struct_spec.rb +0 -1
  31. data/spec/functional/final_var_spec.rb +0 -2
  32. data/spec/functional/memo_spec.rb +7 -10
  33. data/spec/functional/option_spec.rb +0 -1
  34. data/spec/functional/pattern_matching_spec.rb +0 -1
  35. data/spec/functional/protocol_info_spec.rb +0 -2
  36. data/spec/functional/protocol_spec.rb +1 -3
  37. data/spec/functional/record_spec.rb +170 -87
  38. data/spec/functional/tuple_spec.rb +0 -1
  39. data/spec/functional/type_check_spec.rb +0 -2
  40. data/spec/functional/union_spec.rb +0 -1
  41. data/spec/functional/value_struct_spec.rb +0 -1
  42. metadata +14 -29
  43. data/doc/memo.txt +0 -192
  44. data/doc/pattern_matching.txt +0 -485
  45. data/doc/protocol.txt +0 -221
  46. data/doc/record.txt +0 -207
  47. data/doc/thread_safety.txt +0 -17
@@ -1,4 +1,4 @@
1
- require 'thread'
1
+ require 'functional/synchronization'
2
2
 
3
3
  module Functional
4
4
 
@@ -30,15 +30,20 @@ module Functional
30
30
  # f.set? #=> true
31
31
  # f.value #=> 42
32
32
  #
33
- # @since 1.1.0
34
- #
35
33
  # @see Functional::FinalStruct
36
34
  # @see http://en.wikipedia.org/wiki/Final_(Java) Java `final` keyword
37
35
  #
38
- # @!macro thread_safe_final_object
39
- class FinalVar
36
+ # @!macro [new] thread_safe_final_object
37
+ #
38
+ # @note This is a write-once, read-many, thread safe object that can
39
+ # be used in concurrent systems. Thread safety guarantees *cannot* be made
40
+ # about objects contained *within* this object, however. Ruby variables are
41
+ # mutable references to mutable objects. This cannot be changed. The best
42
+ # practice it to only encapsulate immutable, frozen, or thread safe objects.
43
+ # Ultimately, thread safety is the responsibility of the programmer.
44
+ class FinalVar < Synchronization::Object
40
45
 
41
- # @!visibility private
46
+ # @!visibility private
42
47
  NO_VALUE = Object.new.freeze
43
48
 
44
49
  # Create a new `FinalVar` with the given value or "unset" when
@@ -46,17 +51,15 @@ module Functional
46
51
  #
47
52
  # @param [Object] value if given, the immutable value of the object
48
53
  def initialize(value = NO_VALUE)
49
- @mutex = Mutex.new
50
- @value = value
54
+ super
55
+ synchronize{ @value = value }
51
56
  end
52
57
 
53
58
  # Get the current value or nil if unset.
54
59
  #
55
60
  # @return [Object] the current value or nil
56
61
  def get
57
- @mutex.synchronize {
58
- has_been_set? ? @value : nil
59
- }
62
+ synchronize { has_been_set? ? @value : nil }
60
63
  end
61
64
  alias_method :value, :get
62
65
 
@@ -66,13 +69,13 @@ module Functional
66
69
  # @return [Object] the new value
67
70
  # @raise [Functional::FinalityError] if the value has already been set
68
71
  def set(value)
69
- @mutex.synchronize {
72
+ synchronize do
70
73
  if has_been_set?
71
74
  raise FinalityError.new('value has already been set')
72
75
  else
73
76
  @value = value
74
77
  end
75
- }
78
+ end
76
79
  end
77
80
  alias_method :value=, :set
78
81
 
@@ -80,9 +83,7 @@ module Functional
80
83
  #
81
84
  # @return [Boolean] true when the value has been set else false
82
85
  def set?
83
- @mutex.synchronize {
84
- has_been_set?
85
- }
86
+ synchronize { has_been_set? }
86
87
  end
87
88
  alias_method :value?, :set?
88
89
 
@@ -91,13 +92,13 @@ module Functional
91
92
  # @param [Object] value the value to set
92
93
  # @return [Object] the current value if already set else the new value
93
94
  def get_or_set(value)
94
- @mutex.synchronize {
95
+ synchronize do
95
96
  if has_been_set?
96
97
  @value
97
98
  else
98
99
  @value = value
99
100
  end
100
- }
101
+ end
101
102
  end
102
103
 
103
104
  # Get the value if set else return the given default value.
@@ -105,9 +106,7 @@ module Functional
105
106
  # @param [Object] default the value to return if currently unset
106
107
  # @return [Object] the current value when set else the given default
107
108
  def fetch(default)
108
- @mutex.synchronize {
109
- has_been_set? ? @value : default
110
- }
109
+ synchronize { has_been_set? ? @value : default }
111
110
  end
112
111
 
113
112
  # Compares this object and other for equality. A `FinalVar` that is unset
@@ -1,4 +1,4 @@
1
- require 'thread'
1
+ require 'functional/synchronization'
2
2
 
3
3
  module Functional
4
4
 
@@ -9,14 +9,12 @@ module Functional
9
9
  # the cached result. As a result the response time for frequently called
10
10
  # functions is vastly incresed (after the first call with any given set of)
11
11
  # arguments, at the cost of increased memory usage (the cache).
12
- #
13
- # @!macro memoize
14
12
  #
15
- # @note Memoized method calls are thread safe and can safely be used in concurrent systems.
16
- # Declaring memoization on a function is *not* thread safe and should only be done during
17
- # application initialization.
13
+ # {include:file:doc/memo.md}
18
14
  #
19
- # @since 1.0.0
15
+ # @note Memoized method calls are thread safe and can safely be used in
16
+ # concurrent systems. Declaring memoization on a function is *not* thread
17
+ # safe and should only be done during application initialization.
20
18
  module Memo
21
19
 
22
20
  # @!visibility private
@@ -37,24 +35,36 @@ module Functional
37
35
  module ClassMethods
38
36
 
39
37
  # @!visibility private
40
- Memo = Struct.new(:function, :mutex, :cache, :max_cache) do
38
+ class Memoizer < Synchronization::Object
39
+ attr_reader :function, :cache, :max_cache
40
+ def initialize(function, max_cache)
41
+ super
42
+ synchronize do
43
+ @function = function
44
+ @max_cache = max_cache
45
+ @cache = {}
46
+ end
47
+ end
41
48
  def max_cache?
42
49
  max_cache > 0 && cache.size >= max_cache
43
50
  end
51
+ public :synchronize
44
52
  end
53
+ private_constant :Memoizer
45
54
 
46
55
  # @!visibility private
47
56
  attr_accessor :__method_memos__
48
57
 
49
58
  # Returns a memoized version of a referentially transparent function. The
50
- # memoized version of the function keeps a cache of the mapping from arguments
51
- # to results and, when calls with the same arguments are repeated often, has
52
- # higher performance at the expense of higher memory use.
59
+ # memoized version of the function keeps a cache of the mapping from
60
+ # arguments to results and, when calls with the same arguments are
61
+ # repeated often, has higher performance at the expense of higher memory
62
+ # use.
53
63
  #
54
64
  # @param [Symbol] func the class/module function to memoize
55
65
  # @param [Hash] opts the options controlling memoization
56
- # @option opts [Fixnum] :at_most the maximum number of memos to store in the
57
- # cache; a value of zero (the default) or `nil` indicates no limit
66
+ # @option opts [Fixnum] :at_most the maximum number of memos to store in
67
+ # the cache; a value of zero (the default) or `nil` indicates no limit
58
68
  #
59
69
  # @raise [ArgumentError] when the method has already been memoized
60
70
  # @raise [ArgumentError] when :at_most option is a negative number
@@ -63,7 +73,7 @@ module Functional
63
73
  max_cache = opts[:at_most].to_i
64
74
  raise ArgumentError.new("method :#{func} has already been memoized") if __method_memos__.has_key?(func)
65
75
  raise ArgumentError.new(':max_cache must be > 0') if max_cache < 0
66
- __method_memos__[func] = Memo.new(method(func), Mutex.new, {}, max_cache.to_i)
76
+ __method_memos__[func] = Memoizer.new(method(func), max_cache.to_i)
67
77
  __define_memo_proxy__(func)
68
78
  nil
69
79
  end
@@ -80,17 +90,16 @@ module Functional
80
90
  # @!visibility private
81
91
  def __proxy_memoized_method__(func, *args, &block)
82
92
  memo = self.__method_memos__[func]
83
- memo.mutex.lock
84
- if block_given?
85
- memo.function.call(*args, &block)
86
- elsif memo.cache.has_key?(args)
87
- memo.cache[args]
88
- else
89
- result = memo.function.call(*args)
90
- memo.cache[args] = result unless memo.max_cache?
93
+ memo.synchronize do
94
+ if block_given?
95
+ memo.function.call(*args, &block)
96
+ elsif memo.cache.has_key?(args)
97
+ memo.cache[args]
98
+ else
99
+ result = memo.function.call(*args)
100
+ memo.cache[args] = result unless memo.max_cache?
101
+ end
91
102
  end
92
- ensure
93
- memo.mutex.unlock
94
103
  end
95
104
  end
96
105
  end
@@ -6,8 +6,6 @@ module Functional
6
6
  #
7
7
  # Helper functions used when pattern matching runtime arguments against
8
8
  # a method defined with the `defn` function of Functional::PatternMatching.
9
- #
10
- # @since 1.0.0
11
9
  module MethodSignature
12
10
  extend self
13
11
 
@@ -70,5 +68,6 @@ module Functional
70
68
  param == PatternMatching::UNBOUND || param == arg
71
69
  end
72
70
  end
71
+ private_constant :MethodSignature
73
72
  end
74
73
  end
@@ -1,6 +1,7 @@
1
- require_relative 'abstract_struct'
2
- require_relative 'either'
3
- require_relative 'protocol'
1
+ require 'functional/abstract_struct'
2
+ require 'functional/either'
3
+ require 'functional/protocol'
4
+ require 'functional/synchronization'
4
5
 
5
6
  Functional::SpecifyProtocol(:Option) do
6
7
  instance_method :some?, 0
@@ -14,13 +15,10 @@ module Functional
14
15
  # This type is a replacement for the use of nil with better type checks.
15
16
  # It is an immutable data structure that extends `AbstractStruct`.
16
17
  #
17
- # @see Functional::AbstractStruct
18
18
  # @see http://functionaljava.googlecode.com/svn/artifacts/3.0/javadoc/index.html Functional Java
19
19
  #
20
- # @since 1.0.0
21
- #
22
20
  # @!macro thread_safe_immutable_object
23
- class Option
21
+ class Option < Synchronization::Object
24
22
  include AbstractStruct
25
23
 
26
24
  # @!visibility private
@@ -201,11 +199,13 @@ module Functional
201
199
  #
202
200
  # @!visibility private
203
201
  def initialize(value, none, reason = nil)
202
+ super
204
203
  @none = none
205
204
  @reason = none ? reason : nil
206
205
  hsh = none ? {some: nil} : {some: value}
207
206
  set_data_hash(hsh)
208
207
  set_values_array(hsh.values)
208
+ ensure_ivar_visibility!
209
209
  end
210
210
  end
211
211
  end
@@ -1,17 +1,17 @@
1
- require_relative 'method_signature'
1
+ require 'functional/method_signature'
2
2
 
3
3
  module Functional
4
4
 
5
- # As much as I love Ruby I've always been a little disappointed that Ruby doesn't
6
- # support function overloading. Function overloading tends to reduce branching
7
- # and keep function signatures simpler. No sweat, I learned to do without. Then
8
- # I started programming in Erlang. My favorite Erlang feature is, without
9
- # question, pattern matching. Pattern matching is like function overloading
10
- # cranked to 11. So one day I was musing on Twitter that I'd like to see
11
- # Erlang-stype pattern matching in Ruby and one of my friends responded
5
+ # As much as I love Ruby I've always been a little disappointed that Ruby
6
+ # doesn't support function overloading. Function overloading tends to reduce
7
+ # branching and keep function signatures simpler. No sweat, I learned to do
8
+ # without. Then I started programming in Erlang. My favorite Erlang feature
9
+ # is, without question, pattern matching. Pattern matching is like function
10
+ # overloading cranked to 11. So one day I was musing on Twitter that I'd like
11
+ # to see Erlang-stype pattern matching in Ruby and one of my friends responded
12
12
  # "Build it!" So I did. And here it is.
13
13
  #
14
- # @!macro pattern_matching
14
+ # {include:file:doc/pattern_matching.md}
15
15
  module PatternMatching
16
16
 
17
17
  # A parameter that is required but that can take any value.
@@ -40,9 +40,11 @@ module Functional
40
40
  self
41
41
  end
42
42
  end
43
+ private_constant :GuardClause
43
44
 
44
45
  # @!visibility private
45
46
  FunctionPattern = Struct.new(:function, :args, :body, :guard)
47
+ private_constant :FunctionPattern
46
48
 
47
49
  # @!visibility private
48
50
  def __unbound_args__(match, args)
@@ -55,7 +57,7 @@ module Functional
55
57
  argv << args[i][key] if value == UNBOUND
56
58
  end
57
59
  elsif p.is_a?(Hash) || p == UNBOUND || p.is_a?(Class)
58
- argv << args[i]
60
+ argv << args[i]
59
61
  end
60
62
  end
61
63
  argv
@@ -1,4 +1,4 @@
1
- require_relative 'protocol_info'
1
+ require 'functional/protocol_info'
2
2
 
3
3
  module Functional
4
4
 
@@ -55,10 +55,8 @@ module Functional
55
55
  # interrogate a module, class, or object for its type and ancestry, protocols
56
56
  # allow modules, classes, and methods to be interrogated based on their behavior.
57
57
  # It is a logical extension of the `respond_to?` method, but vastly more powerful.
58
- #
59
- # @!macro protocol
60
58
  #
61
- # @since 1.0.0 (formerly "behavior")
59
+ # {include:file:doc/protocol.md}
62
60
  module Protocol
63
61
 
64
62
  # The global registry of specified protocols.
@@ -1,12 +1,12 @@
1
+ require 'functional/synchronization'
2
+
1
3
  module Functional
2
4
 
3
5
  # An immutable object describing a single protocol and capable of building
4
6
  # itself from a block. Used by {Functional#SpecifyProtocol}.
5
7
  #
6
8
  # @see Functional::Protocol
7
- #
8
- # @since 1.0.0
9
- class ProtocolInfo
9
+ class ProtocolInfo < Synchronization::Object
10
10
 
11
11
  # The symbolic name of the protocol
12
12
  attr_reader :name
@@ -22,11 +22,13 @@ module Functional
22
22
  def initialize(name, &specification)
23
23
  raise ArgumentError.new('no block given') unless block_given?
24
24
  raise ArgumentError.new('no name given') if name.nil? || name.empty?
25
+ super
25
26
  @name = name.to_sym
26
27
  @info = Info.new({}, {}, [])
27
28
  self.instance_eval(&specification)
28
29
  @info.each_pair{|col, _| col.freeze}
29
30
  @info.freeze
31
+ ensure_ivar_visibility!
30
32
  self.freeze
31
33
  end
32
34
 
@@ -1,5 +1,6 @@
1
- require_relative 'abstract_struct'
2
- require_relative 'type_check'
1
+ require 'functional/abstract_struct'
2
+ require 'functional/protocol'
3
+ require 'functional/type_check'
3
4
 
4
5
  module Functional
5
6
 
@@ -8,18 +9,17 @@ module Functional
8
9
  # using accessor methods, without having to write an explicit class.
9
10
  # The `Record` module generates new `AbstractStruct` subclasses that hold a
10
11
  # set of fields with a reader method for each field.
11
- #
12
+ #
12
13
  # A `Record` is very similar to a Ruby `Struct` and shares many of its behaviors
13
14
  # and attributes. Unlike a # Ruby `Struct`, a `Record` is immutable: its values
14
15
  # are set at construction and can never be changed. Divergence between the two
15
16
  # classes derive from this core difference.
16
- #
17
- # @!macro record
18
17
  #
19
- # @see Functional::AbstractStruct
20
- # @see Functional::Union
18
+ # {include:file:doc/record.md}
21
19
  #
22
- # @since 1.0.0
20
+ # @see Functional::Union
21
+ # @see Functional::Protocol
22
+ # @see Functional::TypeCheck
23
23
  #
24
24
  # @!macro thread_safe_immutable_object
25
25
  module Record
@@ -28,10 +28,30 @@ module Functional
28
28
  # Create a new record class with the given fields.
29
29
  #
30
30
  # @return [Functional::AbstractStruct] the new record subclass
31
- # @raise [ArgumentError] no fields specified
31
+ # @raise [ArgumentError] no fields specified or an invalid type
32
+ # specification is given
32
33
  def new(*fields, &block)
33
34
  raise ArgumentError.new('no fields provided') if fields.empty?
34
- build(fields, &block)
35
+
36
+ name = nil
37
+ types = nil
38
+
39
+ # check if a name for registration is given
40
+ if fields.first.is_a?(String)
41
+ name = fields.first
42
+ fields = fields[1..fields.length-1]
43
+ end
44
+
45
+ # check for a set of type/protocol specifications
46
+ if fields.size == 1 && fields.first.respond_to?(:to_h)
47
+ types = fields.first
48
+ fields = fields.first.keys
49
+ check_types!(types)
50
+ end
51
+
52
+ build(name, fields, types, &block)
53
+ rescue
54
+ raise ArgumentError.new('invalid specification')
35
55
  end
36
56
 
37
57
  private
@@ -40,14 +60,18 @@ module Functional
40
60
  #
41
61
  # A set of restrictions governing the creation of a new record.
42
62
  class Restrictions
63
+ include Protocol
43
64
  include TypeCheck
44
65
 
45
66
  # Create a new restrictions object by processing the given
46
67
  # block. The block should be the DSL for defining a record class.
47
68
  #
69
+ # @param [Hash] types a hash of fields and the associated type/protocol
70
+ # when type/protocol checking is among the restrictions
48
71
  # @param [Proc] block A DSL definition of a new record.
49
72
  # @yield A DSL definition of a new record.
50
- def initialize(&block)
73
+ def initialize(types = nil, &block)
74
+ @types = types
51
75
  @required = []
52
76
  @defaults = {}
53
77
  instance_eval(&block) if block_given?
@@ -86,18 +110,44 @@ module Functional
86
110
  return value
87
111
  end
88
112
 
113
+ # Validate the record data against this set of restrictions.
114
+ #
115
+ # @param [Hash] data the data hash
116
+ # @raise [ArgumentError] when the data does not match the restrictions
117
+ def validate!(data)
118
+ validate_mandatory!(data)
119
+ validate_types!(data)
120
+ end
121
+
122
+ private
123
+
89
124
  # Check the given data hash to see if it contains non-nil values for
90
125
  # all mandatory fields.
91
126
  #
92
127
  # @param [Hash] data the data hash
93
128
  # @raise [ArgumentError] if any mandatory fields are missing
94
- def check_mandatory!(data)
129
+ def validate_mandatory!(data)
95
130
  if data.any?{|k,v| @required.include?(k) && v.nil? }
96
131
  raise ArgumentError.new('mandatory fields must not be nil')
97
132
  end
98
133
  end
99
134
 
100
- private
135
+ # Validate the record data against a type/protocol specification.
136
+ #
137
+ # @param [Hash] data the data hash
138
+ # @raise [ArgumentError] when the data does not match the specification
139
+ def validate_types!(data)
140
+ return if @types.nil?
141
+ @types.each do |field, type|
142
+ value = data[field]
143
+ next if value.nil?
144
+ if type.is_a? Module
145
+ raise ArgumentError.new("'#{field}' must be of type #{type}") unless Type?(value, type)
146
+ else
147
+ raise ArgumentError.new("'#{field}' must stasify the protocol :#{type}") unless Satisfy?(value, type)
148
+ end
149
+ end
150
+ end
101
151
 
102
152
  # Is the given object uncloneable?
103
153
  #
@@ -107,15 +157,29 @@ module Functional
107
157
  Type? object, NilClass, TrueClass, FalseClass, Fixnum, Bignum, Float
108
158
  end
109
159
  end
160
+ private_constant :Restrictions
161
+
162
+ # Validate the given type/protocol specification.
163
+ #
164
+ # @param [Hash] types the type specification
165
+ # @raise [ArgumentError] when the specification is not valid
166
+ def check_types!(types)
167
+ return if types.nil?
168
+ unless types.all?{|k,v| v.is_a?(Module) || v.is_a?(Symbol) }
169
+ raise ArgumentError.new('invalid specification')
170
+ end
171
+ end
110
172
 
111
173
  # Use the given `AbstractStruct` class and build the methods necessary
112
174
  # to support the given data fields.
113
175
  #
176
+ # @param [String] name the name under which to register the record when given
114
177
  # @param [Array] fields the list of symbolic names for all data fields
115
178
  # @return [Functional::AbstractStruct] the record class
116
- def build(fields, &block)
179
+ def build(name, fields, types, &block)
180
+ fields = [name].concat(fields) unless name.nil?
117
181
  record, fields = AbstractStruct.define_class(self, :record, fields)
118
- record.class_variable_set(:@@restrictions, Restrictions.new(&block))
182
+ record.class_variable_set(:@@restrictions, Restrictions.new(types, &block))
119
183
  define_initializer(record)
120
184
  fields.each do |field|
121
185
  define_reader(record, field)
@@ -129,14 +193,16 @@ module Functional
129
193
  # @return [Functional::AbstractStruct] the record class
130
194
  def define_initializer(record)
131
195
  record.send(:define_method, :initialize) do |data = {}|
196
+ super()
132
197
  restrictions = record.class_variable_get(:@@restrictions)
133
198
  data = record.fields.reduce({}) do |memo, field|
134
199
  memo[field] = data.fetch(field, restrictions.clone_default(field))
135
200
  memo
136
201
  end
137
- restrictions.check_mandatory!(data)
202
+ restrictions.validate!(data)
138
203
  set_data_hash(data)
139
204
  set_values_array(data.values)
205
+ ensure_ivar_visibility!
140
206
  self.freeze
141
207
  end
142
208
  record