functional-ruby 1.1.0 → 1.2.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 (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