functional-ruby 0.7.7 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +92 -152
  3. data/doc/memo.txt +192 -0
  4. data/doc/pattern_matching.txt +485 -0
  5. data/doc/protocol.txt +221 -0
  6. data/doc/record.txt +144 -0
  7. data/doc/thread_safety.txt +8 -0
  8. data/lib/functional.rb +48 -18
  9. data/lib/functional/abstract_struct.rb +161 -0
  10. data/lib/functional/delay.rb +117 -0
  11. data/lib/functional/either.rb +222 -0
  12. data/lib/functional/memo.rb +93 -0
  13. data/lib/functional/method_signature.rb +72 -0
  14. data/lib/functional/option.rb +209 -0
  15. data/lib/functional/pattern_matching.rb +117 -100
  16. data/lib/functional/protocol.rb +157 -0
  17. data/lib/functional/protocol_info.rb +193 -0
  18. data/lib/functional/record.rb +155 -0
  19. data/lib/functional/type_check.rb +112 -0
  20. data/lib/functional/union.rb +152 -0
  21. data/lib/functional/version.rb +3 -1
  22. data/spec/functional/abstract_struct_shared.rb +154 -0
  23. data/spec/functional/complex_pattern_matching_spec.rb +205 -0
  24. data/spec/functional/configuration_spec.rb +17 -0
  25. data/spec/functional/delay_spec.rb +147 -0
  26. data/spec/functional/either_spec.rb +237 -0
  27. data/spec/functional/memo_spec.rb +207 -0
  28. data/spec/functional/option_spec.rb +292 -0
  29. data/spec/functional/pattern_matching_spec.rb +279 -276
  30. data/spec/functional/protocol_info_spec.rb +444 -0
  31. data/spec/functional/protocol_spec.rb +274 -0
  32. data/spec/functional/record_spec.rb +175 -0
  33. data/spec/functional/type_check_spec.rb +103 -0
  34. data/spec/functional/union_spec.rb +110 -0
  35. data/spec/spec_helper.rb +6 -4
  36. metadata +55 -45
  37. data/lib/functional/behavior.rb +0 -138
  38. data/lib/functional/behaviour.rb +0 -2
  39. data/lib/functional/catalog.rb +0 -487
  40. data/lib/functional/collection.rb +0 -403
  41. data/lib/functional/inflect.rb +0 -127
  42. data/lib/functional/platform.rb +0 -120
  43. data/lib/functional/search.rb +0 -132
  44. data/lib/functional/sort.rb +0 -41
  45. data/lib/functional/utilities.rb +0 -189
  46. data/md/behavior.md +0 -188
  47. data/md/catalog.md +0 -32
  48. data/md/collection.md +0 -32
  49. data/md/inflect.md +0 -32
  50. data/md/pattern_matching.md +0 -512
  51. data/md/platform.md +0 -32
  52. data/md/search.md +0 -32
  53. data/md/sort.md +0 -32
  54. data/md/utilities.md +0 -55
  55. data/spec/functional/behavior_spec.rb +0 -528
  56. data/spec/functional/catalog_spec.rb +0 -1206
  57. data/spec/functional/collection_spec.rb +0 -752
  58. data/spec/functional/inflect_spec.rb +0 -85
  59. data/spec/functional/integration_spec.rb +0 -205
  60. data/spec/functional/platform_spec.rb +0 -501
  61. data/spec/functional/search_spec.rb +0 -187
  62. data/spec/functional/sort_spec.rb +0 -61
  63. data/spec/functional/utilities_spec.rb +0 -277
@@ -0,0 +1,157 @@
1
+ require_relative 'protocol_info'
2
+
3
+ module Functional
4
+
5
+ # An exception indicating a problem during protocol processing.
6
+ ProtocolError = Class.new(StandardError)
7
+
8
+ # Specify a new protocol or retrieve the specification of an existing
9
+ # protocol.
10
+ #
11
+ # When called without a block the global protocol registry will be searched
12
+ # for a protocol with the matching name. If found the corresponding
13
+ # {Functional::ProtocolInfo} object will be returned. If not found `nil` will
14
+ # be returned.
15
+ #
16
+ # When called with a block, a new protocol with the given name will be
17
+ # created and the block will be processed to provide the specifiction.
18
+ # When successful the new {Functional::ProtocolInfo} object will be returned.
19
+ # An exception will be raised if a protocol with the same name already
20
+ # exists.
21
+ #
22
+ # @example
23
+ # Functional::SpecifyProtocol(:Queue) do
24
+ # instance_method :push, 1
25
+ # instance_method :pop, 0
26
+ # instance_method :length, 0
27
+ # end
28
+ #
29
+ # @param [Symbol] name The global name of the new protocol
30
+ # @yield The protocol definition
31
+ # @return [Functional::ProtocolInfo] the newly created or already existing
32
+ # protocol specification
33
+ #
34
+ # @raise [Functional::ProtocolError] when attempting to specify a protocol
35
+ # that has already been specified.
36
+ #
37
+ # @see Functional::Protocol
38
+ def SpecifyProtocol(name, &block)
39
+ name = name.to_sym
40
+ protocol_info = Protocol.class_variable_get(:@@info)[name]
41
+
42
+ return protocol_info unless block_given?
43
+
44
+ if block_given? && protocol_info
45
+ raise ProtocolError.new(":#{name} has already been defined")
46
+ end
47
+
48
+ info = ProtocolInfo.new(name, &block)
49
+ Protocol.class_variable_get(:@@info)[name] = info
50
+ end
51
+ module_function :SpecifyProtocol
52
+
53
+ # Protocols provide a polymorphism and method-dispatch mechanism that exchews
54
+ # stong typing and embraces the dynamic duck typing of Ruby. Rather than
55
+ # interrogate a module, class, or object for its type and ancestry, protocols
56
+ # allow modules, classes, and methods to be interrogated based on their behavior.
57
+ # It is a logical extension of the `respond_to?` method, but vastly more powerful.
58
+ #
59
+ # @!macro protocol
60
+ module Protocol
61
+
62
+ # The global registry of specified protocols.
63
+ @@info = {}
64
+
65
+ # Does the given module/class/object fully satisfy the given protocol(s)?
66
+ #
67
+ # @param [Object] target the method/class/object to interrogate
68
+ # @param [Symbol] protocols one or more protocols to check against the target
69
+ # @return [Boolean] true if the target satisfies all given protocols else false
70
+ #
71
+ # @raise [ArgumentError] when no protocols given
72
+ def Satisfy?(target, *protocols)
73
+ raise ArgumentError.new('no protocols given') if protocols.empty?
74
+ protocols.all?{|protocol| Protocol.satisfies?(target, protocol.to_sym) }
75
+ end
76
+ module_function :Satisfy?
77
+
78
+ # Does the given module/class/object fully satisfy the given protocol(s)?
79
+ # Raises a {Functional::ProtocolError} on failure.
80
+ #
81
+ # @param [Object] target the method/class/object to interrogate
82
+ # @param [Symbol] protocols one or more protocols to check against the target
83
+ # @return [Symbol] the target
84
+ #
85
+ # @raise [Functional::ProtocolError] when one or more protocols are not satisfied
86
+ # @raise [ArgumentError] when no protocols given
87
+ def Satisfy!(target, *protocols)
88
+ Protocol::Satisfy?(target, *protocols) or
89
+ Protocol.error(target, 'does not', *protocols)
90
+ target
91
+ end
92
+ module_function :Satisfy!
93
+
94
+ # Have the given protocols been specified?
95
+ #
96
+ # @param [Symbol] protocols the list of protocols to check
97
+ # @return [Boolean] true if all given protocols have been specified else false
98
+ #
99
+ # @raise [ArgumentError] when no protocols are given
100
+ def Specified?(*protocols)
101
+ raise ArgumentError.new('no protocols given') if protocols.empty?
102
+ Protocol.unspecified(*protocols).empty?
103
+ end
104
+ module_function :Specified?
105
+
106
+ # Have the given protocols been specified?
107
+ # Raises a {Functional::ProtocolError} on failure.
108
+ #
109
+ # @param [Symbol] protocols the list of protocols to check
110
+ # @return [Boolean] true if all given protocols have been specified
111
+ #
112
+ # @raise [Functional::ProtocolError] if one or more of the given protocols have
113
+ # not been specified
114
+ # @raise [ArgumentError] when no protocols are given
115
+ def Specified!(*protocols)
116
+ raise ArgumentError.new('no protocols given') if protocols.empty?
117
+ (unspecified = Protocol.unspecified(*protocols)).empty? or
118
+ raise ProtocolError.new("The following protocols are unspecified: :#{unspecified.join('; :')}.")
119
+ end
120
+ module_function :Specified!
121
+
122
+ private
123
+
124
+ # Does the target satisfy the given protocol?
125
+ #
126
+ # @param [Object] target the module/class/object to check
127
+ # @param [Symbol] protocol the protocol to check against the target
128
+ # @return [Boolean] true if the target satisfies the protocol else false
129
+ def self.satisfies?(target, protocol)
130
+ info = @@info[protocol]
131
+ return info && info.satisfies?(target)
132
+ end
133
+
134
+ # Reduces a list of protocols to a list of unspecified protocols.
135
+ #
136
+ # @param [Symbol] protocols the list of protocols to check
137
+ # @return [Array] zero or more unspecified protocols
138
+ def self.unspecified(*protocols)
139
+ protocols.drop_while do |protocol|
140
+ @@info.has_key? protocol.to_sym
141
+ end
142
+ end
143
+
144
+ # Raise a {Functional::ProtocolError} formatted with the given data.
145
+ #
146
+ # @param [Object] target the object that was being interrogated
147
+ # @param [String] message the message fragment to inject into the error
148
+ # @param [Symbol] protocols list of protocols that were being checked against the target
149
+ #
150
+ # @raise [Functional::ProtocolError] the formatted exception object
151
+ def self.error(target, message, *protocols)
152
+ target = target.class unless target.is_a?(Module)
153
+ raise ProtocolError,
154
+ "Value (#{target.class}) '#{target}' #{message} behave as all of: :#{protocols.join('; :')}."
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,193 @@
1
+ module Functional
2
+
3
+ # An immutable object describing a single protocol and capable of building
4
+ # itself from a block. Used by {Functional#SpecifyProtocol}.
5
+ #
6
+ # @see Functional::Protocol
7
+ class ProtocolInfo
8
+
9
+ # The symbolic name of the protocol
10
+ attr_reader :name
11
+
12
+ # Process a protocol specification block and build a new object.
13
+ #
14
+ # @param [Symbol] name the symbolic name of the protocol
15
+ # @yield self to the given specification block
16
+ # @return [Functional::ProtocolInfo] the new info object, frozen
17
+ #
18
+ # @raise [ArgumentError] when name is nil or an empty string
19
+ # @raise [ArgumentError] when no block given
20
+ def initialize(name, &specification)
21
+ raise ArgumentError.new('no block given') unless block_given?
22
+ raise ArgumentError.new('no name given') if name.nil? || name.empty?
23
+ @name = name.to_sym
24
+ @info = Info.new({}, {}, [])
25
+ self.instance_eval(&specification)
26
+ @info.each_pair{|col, _| col.freeze}
27
+ @info.freeze
28
+ self.freeze
29
+ end
30
+
31
+ # The instance methods expected by this protocol.
32
+ #
33
+ # @return [Hash] a frozen hash of all instance method names and their
34
+ # expected arity for this protocol
35
+ def instance_methods
36
+ @info.instance_methods
37
+ end
38
+
39
+ # The class methods expected by this protocol.
40
+ #
41
+ # @return [Hash] a frozen hash of all class method names and their
42
+ # expected arity for this protocol
43
+ def class_methods
44
+ @info.class_methods
45
+ end
46
+
47
+ # The constants expected by this protocol.
48
+ #
49
+ # @return [Array] a frozen list of the constants expected by this protocol
50
+ def constants
51
+ @info.constants
52
+ end
53
+
54
+ # Does the given module/class/object satisfy this protocol?
55
+ #
56
+ # @return [Boolean] true if the target satisfies this protocol else false
57
+ def satisfies?(target)
58
+ satisfies_constants?(target) &&
59
+ satisfies_instance_methods?(target) &&
60
+ satisfies_class_methods?(target)
61
+ end
62
+
63
+ private
64
+
65
+ # Data structure for encapsulating the protocol info data.
66
+ Info = Struct.new(:instance_methods, :class_methods, :constants)
67
+
68
+ # Does the target satisfy the constants expected by this protocol?
69
+ #
70
+ # @param [target] target the module/class/object to interrogate
71
+ # @return [Boolean] true when satisfied else false
72
+ def satisfies_constants?(target)
73
+ clazz = target.is_a?(Module) ? target : target.class
74
+ @info.constants.all?{|constant| clazz.const_defined?(constant) }
75
+ end
76
+
77
+ # Does the target satisfy the instance methods expected by this protocol?
78
+ #
79
+ # @param [target] target the module/class/object to interrogate
80
+ # @return [Boolean] true when satisfied else false
81
+ def satisfies_instance_methods?(target)
82
+ @info.instance_methods.all? do |method, arity|
83
+ if target.is_a? Module
84
+ target.method_defined?(method) && check_arity?(target.instance_method(method), arity)
85
+ else
86
+ target.respond_to?(method) && check_arity?(target.method(method), arity)
87
+ end
88
+ end
89
+ end
90
+
91
+
92
+ # Does the target satisfy the class methods expected by this protocol?
93
+ #
94
+ # @param [target] target the module/class/object to interrogate
95
+ # @return [Boolean] true when satisfied else false
96
+ def satisfies_class_methods?(target)
97
+ clazz = target.is_a?(Module) ? target : target.class
98
+ @info.class_methods.all? do |method, arity|
99
+ break false unless clazz.respond_to? method
100
+ method = clazz.method(method)
101
+ check_arity?(method, arity)
102
+ end
103
+ end
104
+
105
+ # Does the given method have the expected arity? Returns true
106
+ # if the arity of the method is `-1` (variable length argument list
107
+ # with no required arguments), when expected is `nil` (indicating any
108
+ # arity is acceptable), or the arity of the method exactly matches the
109
+ # expected arity.
110
+ #
111
+ # @param [Method] method the method object to interrogate
112
+ # @param [Fixnum] expected the expected arity
113
+ # @return [Boolean] true when an acceptable match else false
114
+ #
115
+ # @see http://www.ruby-doc.org/core-2.1.2/Method.html#method-i-arity Method#arity
116
+ def check_arity?(method, expected)
117
+ arity = method.arity
118
+ expected.nil? || arity == -1 || expected == arity
119
+ end
120
+
121
+ #################################################################
122
+ # DSL methods
123
+
124
+ # Specify an instance method.
125
+ #
126
+ # @param [Symbol] name the name of the method
127
+ # @param [Fixnum] arity the required arity
128
+ def instance_method(name, arity = nil)
129
+ arity = arity.to_i unless arity.nil?
130
+ @info.instance_methods[name.to_sym] = arity
131
+ end
132
+
133
+ # Specify a class method.
134
+ #
135
+ # @param [Symbol] name the name of the method
136
+ # @param [Fixnum] arity the required arity
137
+ def class_method(name, arity = nil)
138
+ arity = arity.to_i unless arity.nil?
139
+ @info.class_methods[name.to_sym] = arity
140
+ end
141
+
142
+ # Specify an instance reader attribute.
143
+ #
144
+ # @param [Symbol] name the name of the attribute
145
+ def attr_reader(name)
146
+ instance_method(name, 0)
147
+ end
148
+
149
+ # Specify an instance writer attribute.
150
+ #
151
+ # @param [Symbol] name the name of the attribute
152
+ def attr_writer(name)
153
+ instance_method("#{name}=".to_sym, 1)
154
+ end
155
+
156
+ # Specify an instance accessor attribute.
157
+ #
158
+ # @param [Symbol] name the name of the attribute
159
+ def attr_accessor(name)
160
+ attr_reader(name)
161
+ attr_writer(name)
162
+ end
163
+
164
+ # Specify a class reader attribute.
165
+ #
166
+ # @param [Symbol] name the name of the attribute
167
+ def class_attr_reader(name)
168
+ class_method(name, 0)
169
+ end
170
+
171
+ # Specify a class writer attribute.
172
+ #
173
+ # @param [Symbol] name the name of the attribute
174
+ def class_attr_writer(name)
175
+ class_method("#{name}=".to_sym, 1)
176
+ end
177
+
178
+ # Specify a class accessor attribute.
179
+ #
180
+ # @param [Symbol] name the name of the attribute
181
+ def class_attr_accessor(name)
182
+ class_attr_reader(name)
183
+ class_attr_writer(name)
184
+ end
185
+
186
+ # Specify a constant.
187
+ #
188
+ # @param [Symbol] name the name of the constant
189
+ def constant(name)
190
+ @info.constants << name.to_sym
191
+ end
192
+ end
193
+ end
@@ -0,0 +1,155 @@
1
+ require_relative 'abstract_struct'
2
+ require_relative 'type_check'
3
+
4
+ module Functional
5
+
6
+ # An immutable data structure with multiple data fields. A `Record` is a
7
+ # convenient way to bundle a number of field attributes together,
8
+ # using accessor methods, without having to write an explicit class.
9
+ # The `Record` module generates new `AbstractStruct` subclasses that hold a
10
+ # set of fields with a reader method for each field.
11
+ #
12
+ # A `Record` is very similar to a Ruby `Struct` and shares many of its behaviors
13
+ # and attributes. Unlike a # Ruby `Struct`, a `Record` is immutable: its values
14
+ # are set at construction and can never be changed. Divergence between the two
15
+ # classes derive from this core difference.
16
+ #
17
+ # @!macro record
18
+ #
19
+ # @see Functional::AbstractStruct
20
+ # @see Functional::Union
21
+ #
22
+ # @!macro thread_safe_immutable_object
23
+ module Record
24
+ extend self
25
+
26
+ # Create a new record class with the given fields.
27
+ #
28
+ # @return [Functional::AbstractStruct] the new record subclass
29
+ # @raise [ArgumentError] no fields specified
30
+ def new(*fields, &block)
31
+ raise ArgumentError.new('no fields provided') if fields.empty?
32
+ build(fields, &block)
33
+ end
34
+
35
+ private
36
+
37
+ # @!visibility private
38
+ #
39
+ # A set of restrictions governing the creation of a new record.
40
+ class Restrictions
41
+ include TypeCheck
42
+
43
+ # Create a new restrictions object by processing the given
44
+ # block. The block should be the DSL for defining a record class.
45
+ #
46
+ # @param [Proc] block A DSL definition of a new record.
47
+ # @yield A DSL definition of a new record.
48
+ def initialize(&block)
49
+ @required = []
50
+ @defaults = {}
51
+ instance_eval(&block) if block_given?
52
+ @required.freeze
53
+ @defaults.freeze
54
+ self.freeze
55
+ end
56
+
57
+ # DSL method for declaring one or more fields to be mandatory.
58
+ #
59
+ # @param [Symbol] fields zero or more mandatory fields
60
+ def mandatory(*fields)
61
+ @required.concat(fields.collect{|field| field.to_sym})
62
+ end
63
+
64
+ # DSL method for declaring a default value for a field
65
+ #
66
+ # @param [Symbol] field the field to be given a default value
67
+ # @param [Object] value the default value of the field
68
+ def default(field, value)
69
+ @defaults[field] = value
70
+ end
71
+
72
+ # Clone a default value if it is cloneable. Else just return
73
+ # the value.
74
+ #
75
+ # @param [Symbol] field the name of the field from which the
76
+ # default value is to be cloned.
77
+ # @return [Object] a clone of the value or the value if uncloneable
78
+ def clone_default(field)
79
+ value = @defaults[field]
80
+ value = value.clone unless uncloneable?(value)
81
+ rescue TypeError
82
+ # can't be cloned
83
+ ensure
84
+ return value
85
+ end
86
+
87
+ # Check the given data hash to see if it contains non-nil values for
88
+ # all mandatory fields.
89
+ #
90
+ # @param [Hash] data the data hash
91
+ # @raise [ArgumentError] if any mandatory fields are missing
92
+ def check_mandatory!(data)
93
+ if data.any?{|k,v| @required.include?(k) && v.nil? }
94
+ raise ArgumentError.new('mandatory fields must not be nil')
95
+ end
96
+ end
97
+
98
+ private
99
+
100
+ # Is the given object uncloneable?
101
+ #
102
+ # @param [Object] object the object to check
103
+ # @return [Boolean] true if the object cannot be cloned else false
104
+ def uncloneable?(object)
105
+ Type? object, NilClass, TrueClass, FalseClass, Fixnum, Bignum, Float
106
+ end
107
+ end
108
+
109
+ # Use the given `AbstractStruct` class and build the methods necessary
110
+ # to support the given data fields.
111
+ #
112
+ # @param [Array] fields the list of symbolic names for all data fields
113
+ # @return [Functional::AbstractStruct] the record class
114
+ def build(fields, &block)
115
+ record, fields = AbstractStruct.define_class(self, :record, fields)
116
+ record.class_variable_set(:@@restrictions, Restrictions.new(&block))
117
+ define_initializer(record)
118
+ fields.each do |field|
119
+ define_reader(record, field)
120
+ end
121
+ record
122
+ end
123
+
124
+ # Define an initializer method on the given record class.
125
+ #
126
+ # @param [Functional::AbstractStruct] record the new record class
127
+ # @return [Functional::AbstractStruct] the record class
128
+ def define_initializer(record)
129
+ record.send(:define_method, :initialize) do |data = {}|
130
+ restrictions = self.class.class_variable_get(:@@restrictions)
131
+ data = fields.reduce({}) do |memo, field|
132
+ memo[field] = data.fetch(field, restrictions.clone_default(field))
133
+ memo
134
+ end
135
+ restrictions.check_mandatory!(data)
136
+ set_data_hash(data)
137
+ set_values_array(data.values)
138
+ self.freeze
139
+ end
140
+ record
141
+ end
142
+
143
+ # Define a reader method on the given record class for the given data field.
144
+ #
145
+ # @param [Functional::AbstractStruct] record the new record class
146
+ # @param [Symbol] field symbolic name of the current data field
147
+ # @return [Functional::AbstractStruct] the record class
148
+ def define_reader(record, field)
149
+ record.send(:define_method, field) do
150
+ to_h[field]
151
+ end
152
+ record
153
+ end
154
+ end
155
+ end