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,5 @@
1
- require_relative 'protocol'
1
+ require 'functional/protocol'
2
+ require 'functional/synchronization'
2
3
 
3
4
  Functional::SpecifyProtocol(:Struct) do
4
5
  instance_method :fields
@@ -11,8 +12,7 @@ end
11
12
  module Functional
12
13
 
13
14
  # An abstract base class for immutable struct classes.
14
- #
15
- # @since 1.0.0
15
+ # @!visibility private
16
16
  module AbstractStruct
17
17
 
18
18
  # @return [Array] the values of all record fields in order, frozen
@@ -94,7 +94,7 @@ module Functional
94
94
  # Set the internal data hash to a copy of the given hash and freeze it.
95
95
  # @param [Hash] data the data hash
96
96
  #
97
- # @!visibility private
97
+ # @!visibility private
98
98
  def set_data_hash(data)
99
99
  @data = data.dup.freeze
100
100
  end
@@ -102,7 +102,7 @@ module Functional
102
102
  # Set the internal values array to a copy of the given array and freeze it.
103
103
  # @param [Array] values the values array
104
104
  #
105
- # @!visibility private
105
+ # @!visibility private
106
106
  def set_values_array(values)
107
107
  @values = values.dup.freeze
108
108
  end
@@ -117,9 +117,9 @@ module Functional
117
117
  # @return [Functional::AbstractStruct, Array] the new class and the
118
118
  # (possibly) updated fields array
119
119
  #
120
- # @!visibility private
120
+ # @!visibility private
121
121
  def self.define_class(parent, datatype, fields)
122
- struct = Class.new{ include AbstractStruct }
122
+ struct = Class.new(Functional::Synchronization::Object){ include AbstractStruct }
123
123
  if fields.first.is_a? String
124
124
  parent.const_set(fields.first, struct)
125
125
  fields = fields[1, fields.length-1]
@@ -157,7 +157,7 @@ module Functional
157
157
  attr_writer :datatype
158
158
 
159
159
  fields = [].freeze
160
- datatype = :struct
160
+ datatype = :struct
161
161
  end
162
162
  end
163
163
  end
@@ -1,26 +1,25 @@
1
- require 'thread'
1
+ require 'functional/synchronization'
2
2
 
3
3
  module Functional
4
4
 
5
- # Lazy evaluation of a block yielding an immutable result. Useful for expensive
6
- # operations that may never be needed.
7
- #
5
+ # Lazy evaluation of a block yielding an immutable result. Useful for
6
+ # expensive operations that may never be needed.
7
+ #
8
8
  # When a `Delay` is created its state is set to `pending`. The value and
9
9
  # reason are both `nil`. The first time the `#value` method is called the
10
10
  # enclosed opration will be run and the calling thread will block. Other
11
11
  # threads attempting to call `#value` will block as well. Once the operation
12
12
  # is complete the *value* will be set to the result of the operation or the
13
13
  # *reason* will be set to the raised exception, as appropriate. All threads
14
- # blocked on `#value` will return. Subsequent calls to `#value` will immediately
15
- # return the cached value. The operation will only be run once. This means that
16
- # any side effects created by the operation will only happen once as well.
17
- #
18
- # @see http://clojuredocs.org/clojure_core/clojure.core/delay Clojure delay
19
- #
20
- # @since 1.0.0
14
+ # blocked on `#value` will return. Subsequent calls to `#value` will
15
+ # immediately return the cached value. The operation will only be run once.
16
+ # This means that any side effects created by the operation will only happen
17
+ # once as well.
21
18
  #
22
19
  # @!macro thread_safe_immutable_object
23
- class Delay
20
+ #
21
+ # @see http://clojuredocs.org/clojure_core/clojure.core/delay Clojure delay
22
+ class Delay < Synchronization::Object
24
23
 
25
24
  # Create a new `Delay` in the `:pending` state.
26
25
  #
@@ -29,79 +28,72 @@ module Functional
29
28
  # @raise [ArgumentError] if no block is given
30
29
  def initialize(&block)
31
30
  raise ArgumentError.new('no block given') unless block_given?
32
- @mutex = Mutex.new
33
- @state = :pending
34
- @task = block
31
+ super
32
+ synchronize do
33
+ @state = :pending
34
+ @task = block
35
+ end
35
36
  end
36
37
 
37
38
  # Current state of block processing.
38
39
  #
39
40
  # @return [Symbol] the current state of block processing
40
41
  def state
41
- @mutex.lock
42
- @state
43
- ensure
44
- @mutex.unlock
42
+ synchronize{ @state }
45
43
  end
46
44
 
47
45
  # The exception raised when processing the block. Returns `nil` if the
48
46
  # operation is still `:pending` or has been `:fulfilled`.
49
47
  #
50
- # @return [StandardError] the exception raised when processing the block else nil
48
+ # @return [StandardError] the exception raised when processing the block
49
+ # else nil.
51
50
  def reason
52
- @mutex.lock
53
- @reason
54
- ensure
55
- @mutex.unlock
51
+ synchronize{ @reason }
56
52
  end
57
53
 
58
54
  # Return the (possibly memoized) value of the delayed operation.
59
- #
55
+ #
60
56
  # If the state is `:pending` then the calling thread will block while the
61
57
  # operation is performed. All other threads simultaneously calling `#value`
62
58
  # will block as well. Once the operation is complete (either `:fulfilled` or
63
59
  # `:rejected`) all waiting threads will unblock and the new value will be
64
60
  # returned.
65
61
  #
66
- # If the state is not `:pending` when `#value` is called the (possibly memoized)
67
- # value will be returned without blocking and without performing the operation
68
- # again.
62
+ # If the state is not `:pending` when `#value` is called the (possibly
63
+ # memoized) value will be returned without blocking and without performing
64
+ # the operation again.
69
65
  #
70
66
  # @return [Object] the (possibly memoized) result of the block operation
71
67
  def value
72
- @mutex.lock
73
- execute_task_once
74
- @value
75
- ensure
76
- @mutex.unlock
68
+ synchronize{ execute_task_once }
77
69
  end
78
70
 
79
71
  # Has the delay been fulfilled?
80
72
  # @return [Boolean]
81
73
  def fulfilled?
82
- state == :fulfilled
74
+ synchronize{ @state == :fulfilled }
83
75
  end
84
76
  alias_method :value?, :fulfilled?
85
77
 
86
78
  # Has the delay been rejected?
87
79
  # @return [Boolean]
88
80
  def rejected?
89
- state == :rejected
81
+ synchronize{ @state == :rejected }
90
82
  end
91
83
  alias_method :reason?, :rejected?
92
84
 
93
85
  # Is delay completion still pending?
94
86
  # @return [Boolean]
95
87
  def pending?
96
- state == :pending
88
+ synchronize{ @state == :pending }
97
89
  end
98
90
 
99
91
  protected
100
92
 
101
93
  # @!visibility private
102
94
  #
103
- # Execute the enclosed task then cache and return the result if
104
- # the current state is pending. Otherwise, return the cached result.
95
+ # Execute the enclosed task then cache and return the result if the current
96
+ # state is pending. Otherwise, return the cached result.
105
97
  #
106
98
  # @return [Object] the result of the block operation
107
99
  def execute_task_once
@@ -114,6 +106,7 @@ module Functional
114
106
  @state = :rejected
115
107
  end
116
108
  end
109
+ @value
117
110
  end
118
111
  end
119
112
  end
@@ -1,5 +1,6 @@
1
- require_relative 'abstract_struct'
2
- require_relative 'protocol'
1
+ require 'functional/abstract_struct'
2
+ require 'functional/protocol'
3
+ require 'functional/synchronization'
3
4
 
4
5
  Functional::SpecifyProtocol(:Either) do
5
6
  instance_method :left, 0
@@ -10,45 +11,47 @@ end
10
11
 
11
12
  module Functional
12
13
 
13
- # The `Either` type represents a value of one of two possible types (a disjoint union).
14
- # It is an immutable structure that contains one and only one value. That value can
15
- # be stored in one of two virtual position, `left` or `right`. The position provides
16
- # context for the encapsulated data.
14
+ # The `Either` type represents a value of one of two possible types (a
15
+ # disjoint union). It is an immutable structure that contains one and only one
16
+ # value. That value can be stored in one of two virtual position, `left` or
17
+ # `right`. The position provides context for the encapsulated data.
17
18
  #
18
- # One of the main uses of `Either` is as a return value that can indicate either
19
- # success or failure. Object oriented programs generally report errors through
20
- # either state or exception handling, neither of which work well in functional
21
- # programming. In the former case, a method is called on an object and when an
22
- # error occurs the state of the object is updated to reflect the error. This does
23
- # not translate well to functional programming because they eschew state and
24
- # mutable objects. In the latter, an exception handling block provides branching
25
- # logic when an exception is thrown. This does not translate well to functional
26
- # programming because it eschews side effects like structured exception handling
27
- # (and structured exception handling tends to be very expensive). `Either` provides
28
- # a powerful and easy-to-use alternative.
19
+ # One of the main uses of `Either` is as a return value that can indicate
20
+ # either success or failure. Object oriented programs generally report errors
21
+ # through either state or exception handling, neither of which work well in
22
+ # functional programming. In the former case, a method is called on an object
23
+ # and when an error occurs the state of the object is updated to reflect the
24
+ # error. This does not translate well to functional programming because they
25
+ # eschew state and mutable objects. In the latter, an exception handling block
26
+ # provides branching logic when an exception is thrown. This does not
27
+ # translate well to functional programming because it eschews side effects
28
+ # like structured exception handling (and structured exception handling tends
29
+ # to be very expensive). `Either` provides a powerful and easy-to-use
30
+ # alternative.
29
31
  #
30
- # A function that may generate an error can choose to return an immutable `Either`
31
- # object in which the position of the value (left or right) indicates the nature
32
- # of the data. By convention, a `left` value indicates an error and a `right` value
33
- # indicates success. This leaves the caller with no ambiguity regarding success or
34
- # failure, requires no persistent state, and does not require expensive exception
35
- # handling facilities.
32
+ # A function that may generate an error can choose to return an immutable
33
+ # `Either` object in which the position of the value (left or right) indicates
34
+ # the nature of the data. By convention, a `left` value indicates an error and
35
+ # a `right` value indicates success. This leaves the caller with no ambiguity
36
+ # regarding success or failure, requires no persistent state, and does not
37
+ # require expensive exception handling facilities.
36
38
  #
37
- # `Either` provides several aliases and convenience functions to facilitate these
38
- # failure/success conventions. The `left` and `right` functions, including their
39
- # derivatives, are mirrored by `reason` and `value`. Failure is indicated by the
40
- # presence of a `reason` and success is indicated by the presence of a `value`.
41
- # When an operation has failed the either is in a `rejected` state, and when an
42
- # operation has successed the either is in a `fulfilled` state. A common convention
43
- # is to use a Ruby `Exception` as the `reason`. The factory method `error` facilitates
44
- # this. The semantics and conventions of `reason`, `value`, and their derivatives
45
- # follow the conventions of the Concurrent Ruby gem.
39
+ # `Either` provides several aliases and convenience functions to facilitate
40
+ # these failure/success conventions. The `left` and `right` functions,
41
+ # including their derivatives, are mirrored by `reason` and `value`. Failure
42
+ # is indicated by the presence of a `reason` and success is indicated by the
43
+ # presence of a `value`. When an operation has failed the either is in a
44
+ # `rejected` state, and when an operation has successed the either is in a
45
+ # `fulfilled` state. A common convention is to use a Ruby `Exception` as the
46
+ # `reason`. The factory method `error` facilitates this. The semantics and
47
+ # conventions of `reason`, `value`, and their derivatives follow the
48
+ # conventions of the Concurrent Ruby gem.
46
49
  #
47
- # The `left`/`right` and `reason`/`value` methods are not mutually exclusive. They
48
- # can be commingled and still result in functionally correct code. This practice
49
- # should be avoided, however. Consistent use of either `left`/`right` or
50
- # `reason`/`value` against each `Either` instance will result in more expressive,
51
- # intent-revealing code.
50
+ # The `left`/`right` and `reason`/`value` methods are not mutually exclusive.
51
+ # They can be commingled and still result in functionally correct code. This
52
+ # practice should be avoided, however. Consistent use of either `left`/`right`
53
+ # or `reason`/`value` against each `Either` instance will result in more
54
+ # expressive, intent-revealing code.
52
55
  #
53
56
  # @example
54
57
  #
@@ -77,16 +80,14 @@ module Functional
77
80
  # @see https://hackage.haskell.org/package/base-4.2.0.1/docs/Data-Either.html Haskell Data.Either
78
81
  # @see http://ruby-concurrency.github.io/concurrent-ruby/Concurrent/Obligation.html Concurrent Ruby
79
82
  #
80
- # @since 1.0.0
81
- #
82
83
  # @!macro thread_safe_immutable_object
83
- class Either
84
+ class Either < Synchronization::Object
84
85
  include AbstractStruct
85
86
 
86
87
  self.datatype = :either
87
88
  self.fields = [:left, :right].freeze
88
89
 
89
- # @!visibility private
90
+ # @!visibility private
90
91
  NO_VALUE = Object.new.freeze
91
92
 
92
93
  private_class_method :new
@@ -119,7 +120,7 @@ module Functional
119
120
  # given error class will be used.
120
121
  #
121
122
  # @example
122
- #
123
+ #
123
124
  # either = Functional::Either.error("You're a bad monkey, Mojo Jojo")
124
125
  # either.fulfilled? #=> false
125
126
  # either.rejected? #=> true
@@ -137,7 +138,7 @@ module Functional
137
138
  end
138
139
 
139
140
  # Projects this either as a left.
140
- #
141
+ #
141
142
  # @return [Object] The left value or `nil` when `right`.
142
143
  def left
143
144
  left? ? to_h[:left] : nil
@@ -145,7 +146,7 @@ module Functional
145
146
  alias_method :reason, :left
146
147
 
147
148
  # Projects this either as a right.
148
- #
149
+ #
149
150
  # @return [Object] The right value or `nil` when `left`.
150
151
  def right
151
152
  right? ? to_h[:right] : nil
@@ -213,12 +214,14 @@ module Functional
213
214
  # @param [Object] value the value of this either
214
215
  # @param [Boolean] is_left is this a left either or right?
215
216
  #
216
- # @!visibility private
217
+ # @!visibility private
217
218
  def initialize(value, is_left)
219
+ super
218
220
  @is_left = is_left
219
221
  hsh = is_left ? {left: value, right: nil} : {left: nil, right: value}
220
222
  set_data_hash(hsh)
221
223
  set_values_array(hsh.values)
224
+ ensure_ivar_visibility!
222
225
  end
223
226
  end
224
227
  end
@@ -1,5 +1,5 @@
1
- require_relative 'final_var'
2
- require 'thread'
1
+ require 'functional/final_var'
2
+ require 'functional/synchronization'
3
3
 
4
4
  module Functional
5
5
 
@@ -41,14 +41,11 @@ module Functional
41
41
  #
42
42
  # name.first = 'Sam' #=> Functional::FinalityError: final accessor 'first' has already been set
43
43
  #
44
- # @since 1.1.0
45
- #
46
- # @see Functional::FinalVar
47
44
  # @see http://www.ruby-doc.org/stdlib-2.1.2/libdoc/ostruct/rdoc/OpenStruct.html
48
45
  # @see http://en.wikipedia.org/wiki/Final_(Java) Java `final` keyword
49
46
  #
50
47
  # @!macro thread_safe_final_object
51
- class FinalStruct
48
+ class FinalStruct < Synchronization::Object
52
49
 
53
50
  # Creates a new `FinalStruct` object. By default, the resulting `FinalStruct`
54
51
  # object will have no attributes. The optional hash, if given, will generate
@@ -57,10 +54,12 @@ module Functional
57
54
  # @param [Hash] attributes the field/value pairs to set on creation
58
55
  def initialize(attributes = {})
59
56
  raise ArgumentError.new('attributes must be given as a hash or not at all') unless attributes.respond_to?(:to_h)
60
- @mutex = Mutex.new
61
- @attribute_hash = {}
62
- attributes.to_h.each_pair do |field, value|
63
- set_attribute(field, value)
57
+ super
58
+ synchronize do
59
+ @attribute_hash = {}
60
+ attributes.to_h.each_pair do |field, value|
61
+ ns_set_attribute(field, value)
62
+ end
64
63
  end
65
64
  end
66
65
 
@@ -71,9 +70,7 @@ module Functional
71
70
  # @param [Symbol] field the field to retrieve the value for
72
71
  # @return [Object] the value of the field is set else nil
73
72
  def get(field)
74
- @mutex.synchronize {
75
- get_attribute(field)
76
- }
73
+ synchronize { ns_get_attribute(field) }
77
74
  end
78
75
  alias_method :[], :get
79
76
 
@@ -91,13 +88,13 @@ module Functional
91
88
  #
92
89
  # @raise [Functional::FinalityError] if the given field has already been set
93
90
  def set(field, value)
94
- @mutex.synchronize {
95
- if attribute_has_been_set?(field)
91
+ synchronize do
92
+ if ns_attribute_has_been_set?(field)
96
93
  raise FinalityError.new("final accessor '#{field}' has already been set")
97
94
  else
98
- set_attribute(field, value)
95
+ ns_set_attribute(field, value)
99
96
  end
100
- }
97
+ end
101
98
  end
102
99
  alias_method :[]=, :set
103
100
 
@@ -109,9 +106,7 @@ module Functional
109
106
  # @param [Symbol] field the field to get the value for
110
107
  # @return [Boolean] true if the field has been set else false
111
108
  def set?(field)
112
- @mutex.synchronize {
113
- attribute_has_been_set?(field)
114
- }
109
+ synchronize { ns_attribute_has_been_set?(field) }
115
110
  end
116
111
 
117
112
  # Get the current value of the given field if already set else set the value of
@@ -121,9 +116,7 @@ module Functional
121
116
  # @param [Object] value the value to set the field to when not previously set
122
117
  # @return [Object] the final value of the given field
123
118
  def get_or_set(field, value)
124
- @mutex.synchronize {
125
- attribute_has_been_set?(field) ? get_attribute(field) : set_attribute(field, value)
126
- }
119
+ synchronize { ns_attribute_has_been_set?(field) ? ns_get_attribute(field) : ns_set_attribute(field, value) }
127
120
  end
128
121
 
129
122
  # Get the current value of the given field if already set else return the given
@@ -133,9 +126,7 @@ module Functional
133
126
  # @param [Object] default the value to return if the field has not been set
134
127
  # @return [Object] the value of the given field else the given default value
135
128
  def fetch(field, default)
136
- @mutex.synchronize {
137
- attribute_has_been_set?(field) ? get_attribute(field) : default
138
- }
129
+ synchronize { ns_attribute_has_been_set?(field) ? ns_get_attribute(field) : default }
139
130
  end
140
131
 
141
132
  # Calls the block once for each attribute, passing the key/value pair as parameters.
@@ -147,11 +138,11 @@ module Functional
147
138
  # @return [Enumerable] when no block is given
148
139
  def each_pair
149
140
  return enum_for(:each_pair) unless block_given?
150
- @mutex.synchronize {
141
+ synchronize do
151
142
  @attribute_hash.each do |field, value|
152
143
  yield(field, value)
153
144
  end
154
- }
145
+ end
155
146
  end
156
147
 
157
148
  # Converts the `FinalStruct` to a `Hash` with keys representing each attribute
@@ -159,9 +150,7 @@ module Functional
159
150
  #
160
151
  # @return [Hash] a `Hash` representing this struct
161
152
  def to_h
162
- @mutex.synchronize {
163
- @attribute_hash.dup
164
- }
153
+ synchronize { @attribute_hash.dup }
165
154
  end
166
155
 
167
156
  # Compares this object and other for equality. A `FinalStruct` is `eql?` to
@@ -190,19 +179,19 @@ module Functional
190
179
 
191
180
  # @!macro final_struct_get_method
192
181
  # @!visibility private
193
- def get_attribute(field)
182
+ def ns_get_attribute(field)
194
183
  @attribute_hash[field.to_sym]
195
184
  end
196
185
 
197
186
  # @!macro final_struct_set_method
198
187
  # @!visibility private
199
- def set_attribute(field, value)
188
+ def ns_set_attribute(field, value)
200
189
  @attribute_hash[field.to_sym] = value
201
190
  end
202
191
 
203
192
  # @!macro final_struct_set_predicate
204
193
  # @!visibility private
205
- def attribute_has_been_set?(field)
194
+ def ns_attribute_has_been_set?(field)
206
195
  @attribute_hash.has_key?(field.to_sym)
207
196
  end
208
197