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,161 @@
1
+ require_relative 'protocol'
2
+
3
+ Functional::SpecifyProtocol(:Struct) do
4
+ instance_method :fields
5
+ instance_method :values
6
+ instance_method :length
7
+ instance_method :each
8
+ instance_method :each_pair
9
+ end
10
+
11
+ module Functional
12
+
13
+ # An abstract base class for immutable struct classes.
14
+ module AbstractStruct
15
+
16
+ # @return [Array] the values of all record fields in order, frozen
17
+ attr_reader :values
18
+
19
+ # Yields the value of each record field in order.
20
+ # If no block is given an enumerator is returned.
21
+ #
22
+ # @yieldparam [Object] value the value of the given field
23
+ #
24
+ # @return [Enumerable] when no block is given
25
+ def each
26
+ return enum_for(:each) unless block_given?
27
+ fields.each do |field|
28
+ yield(self.send(field))
29
+ end
30
+ end
31
+
32
+ # Yields the name and value of each record field in order.
33
+ # If no block is given an enumerator is returned.
34
+ #
35
+ # @yieldparam [Symbol] field the record field for the current iteration
36
+ # @yieldparam [Object] value the value of the current field
37
+ #
38
+ # @return [Enumerable] when no block is given
39
+ def each_pair
40
+ return enum_for(:each_pair) unless block_given?
41
+ fields.each do |field|
42
+ yield(field, self.send(field))
43
+ end
44
+ end
45
+
46
+ # Equality--Returns `true` if `other` has the same record subclass and has equal
47
+ # field values (according to `Object#==`).
48
+ #
49
+ # @param [Object] other the other record to compare for equality
50
+ # @return [Booleab] true when equal else false
51
+ def eql?(other)
52
+ self.class == other.class && self.to_h == other.to_h
53
+ end
54
+ alias_method :==, :eql?
55
+
56
+ # @!macro [attach] inspect_method
57
+ #
58
+ # Describe the contents of this record in a string. Will include the name of the
59
+ # record class, all fields, and all values.
60
+ #
61
+ # @return [String] the class and contents of this record
62
+ def inspect
63
+ state = to_h.to_s.gsub(/^{/, '').gsub(/}$/, '')
64
+ "#<#{self.class.datatype} #{self.class} #{state}>"
65
+ end
66
+ alias_method :to_s, :inspect
67
+
68
+ # Returns the number of record fields.
69
+ #
70
+ # @return [Fixnum] the number of record fields
71
+ def length
72
+ fields.length
73
+ end
74
+ alias_method :size, :length
75
+
76
+ # A frozen array of all record fields.
77
+ #
78
+ # @return [Array] all record fields in order, frozen
79
+ def fields
80
+ self.class.fields
81
+ end
82
+
83
+ # Returns a Hash containing the names and values for the record’s fields.
84
+ #
85
+ # @return [Hash] collection of all fields and their associated values
86
+ def to_h
87
+ @data
88
+ end
89
+
90
+ protected
91
+
92
+ # Set the internal data hash to a copy of the given hash and freeze it.
93
+ # @param [Hash] data the data hash
94
+ #
95
+ # @!visibility private
96
+ def set_data_hash(data)
97
+ @data = data.dup.freeze
98
+ end
99
+
100
+ # Set the internal values array to a copy of the given array and freeze it.
101
+ # @param [Array] values the values array
102
+ #
103
+ # @!visibility private
104
+ def set_values_array(values)
105
+ @values = values.dup.freeze
106
+ end
107
+
108
+ # Define a new struct class and, if necessary, register it with
109
+ # the calling class/module. Will also set the datatype and fields
110
+ # class attributes on the new struct class.
111
+ #
112
+ # @param [Module] parent the class/module that is defining the new struct
113
+ # @param [Symbol] datatype the datatype value for the new struct class
114
+ # @param [Array] fields the list of symbolic names for all data fields
115
+ # @return [Functional::AbstractStruct, Array] the new class and the
116
+ # (possibly) updated fields array
117
+ #
118
+ # @!visibility private
119
+ def self.define_class(parent, datatype, fields)
120
+ struct = Class.new{ include AbstractStruct }
121
+ if fields.first.is_a? String
122
+ parent.const_set(fields.first, struct)
123
+ fields = fields[1, fields.length-1]
124
+ end
125
+ fields = fields.collect{|field| field.to_sym }.freeze
126
+ struct.send(:datatype=, datatype.to_sym)
127
+ struct.send(:fields=, fields)
128
+ [struct, fields]
129
+ end
130
+
131
+ private
132
+
133
+ def self.included(base)
134
+ base.extend(ClassMethods)
135
+ super(base)
136
+ end
137
+
138
+ # Class methods added to a class that includes {Functional::PatternMatching}
139
+ #
140
+ # @!visibility private
141
+ module ClassMethods
142
+
143
+ # A frozen Array of all record fields in order
144
+ attr_reader :fields
145
+
146
+ # A symbol describing the object's datatype
147
+ attr_reader :datatype
148
+
149
+ private
150
+
151
+ # A frozen Array of all record fields in order
152
+ attr_writer :fields
153
+
154
+ # A symbol describing the object's datatype
155
+ attr_writer :datatype
156
+
157
+ fields = [].freeze
158
+ datatype = :struct
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,117 @@
1
+ require 'thread'
2
+
3
+ module Functional
4
+
5
+ # Lazy evaluation of a block yielding an immutable result. Useful for expensive
6
+ # operations that may never be needed.
7
+ #
8
+ # When a `Delay` is created its state is set to `pending`. The value and
9
+ # reason are both `nil`. The first time the `#value` method is called the
10
+ # enclosed opration will be run and the calling thread will block. Other
11
+ # threads attempting to call `#value` will block as well. Once the operation
12
+ # is complete the *value* will be set to the result of the operation or the
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
+ # @!macro thread_safe_immutable_object
21
+ class Delay
22
+
23
+ # Create a new `Delay` in the `:pending` state.
24
+ #
25
+ # @yield the delayed operation to perform
26
+ #
27
+ # @raise [ArgumentError] if no block is given
28
+ def initialize(&block)
29
+ raise ArgumentError.new('no block given') unless block_given?
30
+ @mutex = Mutex.new
31
+ @state = :pending
32
+ @task = block
33
+ end
34
+
35
+ # Current state of block processing.
36
+ #
37
+ # @return [Symbol] the current state of block processing
38
+ def state
39
+ @mutex.lock
40
+ @state
41
+ ensure
42
+ @mutex.unlock
43
+ end
44
+
45
+ # The exception raised when processing the block. Returns `nil` if the
46
+ # operation is still `:pending` or has been `:fulfilled`.
47
+ #
48
+ # @return [StandardError] the exception raised when processing the block else nil
49
+ def reason
50
+ @mutex.lock
51
+ @reason
52
+ ensure
53
+ @mutex.unlock
54
+ end
55
+
56
+ # Return the (possibly memoized) value of the delayed operation.
57
+ #
58
+ # If the state is `:pending` then the calling thread will block while the
59
+ # operation is performed. All other threads simultaneously calling `#value`
60
+ # will block as well. Once the operation is complete (either `:fulfilled` or
61
+ # `:rejected`) all waiting threads will unblock and the new value will be
62
+ # returned.
63
+ #
64
+ # If the state is not `:pending` when `#value` is called the (possibly memoized)
65
+ # value will be returned without blocking and without performing the operation
66
+ # again.
67
+ #
68
+ # @return [Object] the (possibly memoized) result of the block operation
69
+ def value
70
+ @mutex.lock
71
+ execute_task_once
72
+ @value
73
+ ensure
74
+ @mutex.unlock
75
+ end
76
+
77
+ # Has the delay been fulfilled?
78
+ # @return [Boolean]
79
+ def fulfilled?
80
+ state == :fulfilled
81
+ end
82
+ alias_method :value?, :fulfilled?
83
+
84
+ # Has the delay been rejected?
85
+ # @return [Boolean]
86
+ def rejected?
87
+ state == :rejected
88
+ end
89
+ alias_method :reason?, :rejected?
90
+
91
+ # Is delay completion still pending?
92
+ # @return [Boolean]
93
+ def pending?
94
+ state == :pending
95
+ end
96
+
97
+ protected
98
+
99
+ # @!visibility private
100
+ #
101
+ # Execute the enclosed task then cache and return the result if
102
+ # the current state is pending. Otherwise, return the cached result.
103
+ #
104
+ # @return [Object] the result of the block operation
105
+ def execute_task_once
106
+ if @state == :pending
107
+ begin
108
+ @value = @task.call
109
+ @state = :fulfilled
110
+ rescue => ex
111
+ @reason = ex
112
+ @state = :rejected
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,222 @@
1
+ require_relative 'abstract_struct'
2
+ require_relative 'protocol'
3
+
4
+ Functional::SpecifyProtocol(:Either) do
5
+ instance_method :left, 0
6
+ instance_method :left?, 0
7
+ instance_method :right, 0
8
+ instance_method :right?, 0
9
+ end
10
+
11
+ module Functional
12
+
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.
17
+ #
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.
29
+ #
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.
36
+ #
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.
46
+ #
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.
52
+ #
53
+ # @example
54
+ #
55
+ # require 'uri'
56
+ #
57
+ # def web_host(url)
58
+ # uri = URI(url)
59
+ # if uri.scheme == 'http'
60
+ # Functional::Either.left(uri.host)
61
+ # else
62
+ # Functional::Either.right('Invalid HTTP URL')
63
+ # end
64
+ # end
65
+ #
66
+ # good = web_host('http://www.concurrent-ruby.com')
67
+ # good.left? #=> true
68
+ # good.left #=> "www.concurrent-ruby"
69
+ # good.right #=> nil
70
+ #
71
+ # good = web_host('bogus')
72
+ # good.left? #=> false
73
+ # good.left #=> nil
74
+ # good.right #=> "Invalid HTTP URL"
75
+ #
76
+ # @see http://functionaljava.googlecode.com/svn/artifacts/3.0/javadoc/fj/data/Either.html Functional Java
77
+ # @see https://hackage.haskell.org/package/base-4.2.0.1/docs/Data-Either.html Haskell Data.Either
78
+ # @see http://ruby-concurrency.github.io/concurrent-ruby/Concurrent/Obligation.html Concurrent Ruby
79
+ #
80
+ # @!macro thread_safe_immutable_object
81
+ class Either
82
+ include AbstractStruct
83
+
84
+ self.datatype = :either
85
+ self.fields = [:left, :right].freeze
86
+
87
+ # @!visibility private
88
+ NO_VALUE = Object.new.freeze
89
+
90
+ private_class_method :new
91
+
92
+ class << self
93
+
94
+ # Construct a left value of either.
95
+ #
96
+ # @param [Object] value The value underlying the either.
97
+ # @return [Either] A new either with the given left value.
98
+ def left(value)
99
+ new(value, true).freeze
100
+ end
101
+ alias_method :reason, :left
102
+
103
+ # Construct a right value of either.
104
+ #
105
+ # @param [Object] value The value underlying the either.
106
+ # @return [Either] A new either with the given right value.
107
+ def right(value)
108
+ new(value, false).freeze
109
+ end
110
+ alias_method :value, :right
111
+
112
+ # Create an `Either` with the left value set to an `Exception` object
113
+ # complete with message and backtrace. This is a convenience method for
114
+ # supporting the reason/value convention with the reason always being
115
+ # an `Exception` object. When no exception class is given `StandardError`
116
+ # will be used. When no message is given the default message for the
117
+ # given error class will be used.
118
+ #
119
+ # @example
120
+ #
121
+ # either = Functional::Either.error("You're a bad monkey, Mojo Jojo")
122
+ # either.fulfilled? #=> false
123
+ # either.rejected? #=> true
124
+ # either.value #=> nil
125
+ # either.reason #=> #<StandardError: You're a bad monkey, Mojo Jojo>
126
+ #
127
+ # @param [String] message The message for the new error object.
128
+ # @param [Exception] clazz The class for the new error object.
129
+ # @return [Either] A new either with an error object as the left value.
130
+ def error(message = nil, clazz = StandardError)
131
+ ex = clazz.new(message)
132
+ ex.set_backtrace(caller)
133
+ left(ex)
134
+ end
135
+ end
136
+
137
+ # Projects this either as a left.
138
+ #
139
+ # @return [Object] The left value or `nil` when `right`.
140
+ def left
141
+ left? ? to_h[:left] : nil
142
+ end
143
+ alias_method :reason, :left
144
+
145
+ # Projects this either as a right.
146
+ #
147
+ # @return [Object] The right value or `nil` when `left`.
148
+ def right
149
+ right? ? to_h[:right] : nil
150
+ end
151
+ alias_method :value, :right
152
+
153
+ # Returns true if this either is a left, false otherwise.
154
+ #
155
+ # @return [Boolean] `true` if this either is a left, `false` otherwise.
156
+ def left?
157
+ @is_left
158
+ end
159
+ alias_method :reason?, :left?
160
+ alias_method :rejected?, :left?
161
+
162
+ # Returns true if this either is a right, false otherwise.
163
+ #
164
+ # @return [Boolean] `true` if this either is a right, `false` otherwise.
165
+ def right?
166
+ ! left?
167
+ end
168
+ alias_method :value?, :right?
169
+ alias_method :fulfilled?, :right?
170
+
171
+ # If this is a left, then return the left value in right, or vice versa.
172
+ #
173
+ # @return [Either] The value of this either swapped to the opposing side.
174
+ def swap
175
+ if left?
176
+ self.class.send(:new, left, false)
177
+ else
178
+ self.class.send(:new, right, true)
179
+ end
180
+ end
181
+
182
+ # The catamorphism for either. Folds over this either breaking into left or right.
183
+ #
184
+ # @param [Proc] lproc The function to call if this is left.
185
+ # @param [Proc] rproc The function to call if this is right.
186
+ # @return [Object] The reduced value.
187
+ def either(lproc, rproc)
188
+ left? ? lproc.call(left) : rproc.call(right)
189
+ end
190
+
191
+ # If the condition satisfies, return the given A in left, otherwise, return the given B in right.
192
+ #
193
+ # @param [Object] lvalue The left value to use if the condition satisfies.
194
+ # @param [Object] rvalue The right value to use if the condition does not satisfy.
195
+ # @param [Boolean] condition The condition to test (when no block given).
196
+ # @yield The condition to test (when no condition given).
197
+ #
198
+ # @return [Either] A constructed either based on the given condition.
199
+ #
200
+ # @raise [ArgumentError] When both a condition and a block are given.
201
+ def self.iff(lvalue, rvalue, condition = NO_VALUE)
202
+ raise ArgumentError.new('requires either a condition or a block, not both') if condition != NO_VALUE && block_given?
203
+ condition = block_given? ? yield : !! condition
204
+ condition ? left(lvalue) : right(rvalue)
205
+ end
206
+
207
+ private
208
+
209
+ # Create a new Either wil the given value and disposition.
210
+ #
211
+ # @param [Object] value the value of this either
212
+ # @param [Boolean] is_left is this a left either or right?
213
+ #
214
+ # @!visibility private
215
+ def initialize(value, is_left)
216
+ @is_left = is_left
217
+ hsh = is_left ? {left: value, right: nil} : {left: nil, right: value}
218
+ set_data_hash(hsh)
219
+ set_values_array(hsh.values)
220
+ end
221
+ end
222
+ end