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,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