functional-ruby 0.7.7 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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