functional-ruby 0.7.7 → 1.0.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 (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