functional-ruby 1.0.0 → 1.1.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +22 -0
- data/README.md +37 -17
- data/doc/record.txt +63 -0
- data/doc/thread_safety.txt +9 -0
- data/lib/functional.rb +4 -0
- data/lib/functional/abstract_struct.rb +4 -2
- data/lib/functional/delay.rb +2 -0
- data/lib/functional/either.rb +2 -0
- data/lib/functional/final_struct.rb +231 -0
- data/lib/functional/final_var.rb +163 -0
- data/lib/functional/memo.rb +4 -0
- data/lib/functional/method_signature.rb +2 -0
- data/lib/functional/option.rb +2 -0
- data/lib/functional/pattern_matching.rb +1 -0
- data/lib/functional/protocol.rb +2 -0
- data/lib/functional/protocol_info.rb +3 -0
- data/lib/functional/record.rb +4 -2
- data/lib/functional/tuple.rb +247 -0
- data/lib/functional/type_check.rb +2 -0
- data/lib/functional/union.rb +2 -0
- data/lib/functional/value_struct.rb +144 -0
- data/lib/functional/version.rb +1 -1
- data/spec/functional/final_struct_spec.rb +266 -0
- data/spec/functional/final_var_spec.rb +169 -0
- data/spec/functional/record_spec.rb +30 -1
- data/spec/functional/tuple_spec.rb +679 -0
- data/spec/functional/value_struct_spec.rb +199 -0
- metadata +19 -4
@@ -0,0 +1,163 @@
|
|
1
|
+
require 'thread'
|
2
|
+
|
3
|
+
module Functional
|
4
|
+
|
5
|
+
# An exception raised when an attempt is made to modify an
|
6
|
+
# immutable object or attribute.
|
7
|
+
FinalityError = Class.new(StandardError)
|
8
|
+
|
9
|
+
# A thread safe object that holds a single value and is "final" (meaning
|
10
|
+
# that the value can be set at most once after which it becomes immutable).
|
11
|
+
# The value can be set at instantiation which will result in the object
|
12
|
+
# becoming fully and immediately immutable. Attempting to set the value
|
13
|
+
# once it has been set is a logical error and will result in an exception
|
14
|
+
# being raised.
|
15
|
+
#
|
16
|
+
# @example Instanciation With No Value
|
17
|
+
# f = Functional::FinalVar.new
|
18
|
+
# #=> #<Functional::FinalVar unset>
|
19
|
+
# f.set? #=> false
|
20
|
+
# f.value #=> nil
|
21
|
+
# f.value = 42 #=> 42
|
22
|
+
# f.inspect
|
23
|
+
# #=> "#<Functional::FinalVar value=42>"
|
24
|
+
# f.set? #=> true
|
25
|
+
# f.value #=> 42
|
26
|
+
#
|
27
|
+
# @example Instanciation With an Initial Value
|
28
|
+
# f = Functional::FinalVar.new(42)
|
29
|
+
# #=> #<Functional::FinalVar value=42>
|
30
|
+
# f.set? #=> true
|
31
|
+
# f.value #=> 42
|
32
|
+
#
|
33
|
+
# @since 1.1.0
|
34
|
+
#
|
35
|
+
# @see Functional::FinalStruct
|
36
|
+
# @see http://en.wikipedia.org/wiki/Final_(Java) Java `final` keyword
|
37
|
+
#
|
38
|
+
# @!macro thread_safe_final_object
|
39
|
+
class FinalVar
|
40
|
+
|
41
|
+
# @!visibility private
|
42
|
+
NO_VALUE = Object.new.freeze
|
43
|
+
|
44
|
+
# Create a new `FinalVar` with the given value or "unset" when
|
45
|
+
# no value is given.
|
46
|
+
#
|
47
|
+
# @param [Object] value if given, the immutable value of the object
|
48
|
+
def initialize(value = NO_VALUE)
|
49
|
+
@mutex = Mutex.new
|
50
|
+
@value = value
|
51
|
+
end
|
52
|
+
|
53
|
+
# Get the current value or nil if unset.
|
54
|
+
#
|
55
|
+
# @return [Object] the current value or nil
|
56
|
+
def get
|
57
|
+
@mutex.synchronize {
|
58
|
+
has_been_set? ? @value : nil
|
59
|
+
}
|
60
|
+
end
|
61
|
+
alias_method :value, :get
|
62
|
+
|
63
|
+
# Set the value. Will raise an exception if already set.
|
64
|
+
#
|
65
|
+
# @param [Object] value the value to set
|
66
|
+
# @return [Object] the new value
|
67
|
+
# @raise [Functional::FinalityError] if the value has already been set
|
68
|
+
def set(value)
|
69
|
+
@mutex.synchronize {
|
70
|
+
if has_been_set?
|
71
|
+
raise FinalityError.new('value has already been set')
|
72
|
+
else
|
73
|
+
@value = value
|
74
|
+
end
|
75
|
+
}
|
76
|
+
end
|
77
|
+
alias_method :value=, :set
|
78
|
+
|
79
|
+
# Has the value been set?
|
80
|
+
#
|
81
|
+
# @return [Boolean] true when the value has been set else false
|
82
|
+
def set?
|
83
|
+
@mutex.synchronize {
|
84
|
+
has_been_set?
|
85
|
+
}
|
86
|
+
end
|
87
|
+
alias_method :value?, :set?
|
88
|
+
|
89
|
+
# Get the value if it has been set else set the value.
|
90
|
+
#
|
91
|
+
# @param [Object] value the value to set
|
92
|
+
# @return [Object] the current value if already set else the new value
|
93
|
+
def get_or_set(value)
|
94
|
+
@mutex.synchronize {
|
95
|
+
if has_been_set?
|
96
|
+
@value
|
97
|
+
else
|
98
|
+
@value = value
|
99
|
+
end
|
100
|
+
}
|
101
|
+
end
|
102
|
+
|
103
|
+
# Get the value if set else return the given default value.
|
104
|
+
#
|
105
|
+
# @param [Object] default the value to return if currently unset
|
106
|
+
# @return [Object] the current value when set else the given default
|
107
|
+
def fetch(default)
|
108
|
+
@mutex.synchronize {
|
109
|
+
has_been_set? ? @value : default
|
110
|
+
}
|
111
|
+
end
|
112
|
+
|
113
|
+
# Compares this object and other for equality. A `FinalVar` that is unset
|
114
|
+
# is never equal to anything else (it represents a complete absence of value).
|
115
|
+
# When set a `FinalVar` is equal to another `FinalVar` if they have the same
|
116
|
+
# value. A `FinalVar` is equal to another object if its value is equal to
|
117
|
+
# the other object using Ruby's normal equality rules.
|
118
|
+
#
|
119
|
+
# @param [Object] other the object to compare equality to
|
120
|
+
# @return [Boolean] true if equal else false
|
121
|
+
def eql?(other)
|
122
|
+
if (val = fetch(NO_VALUE)) == NO_VALUE
|
123
|
+
false
|
124
|
+
elsif other.is_a?(FinalVar)
|
125
|
+
val == other.value
|
126
|
+
else
|
127
|
+
val == other
|
128
|
+
end
|
129
|
+
end
|
130
|
+
alias_method :==, :eql?
|
131
|
+
|
132
|
+
# Describe the contents of this object in a string.
|
133
|
+
#
|
134
|
+
# @return [String] the string representation of this object
|
135
|
+
#
|
136
|
+
# @!visibility private
|
137
|
+
def inspect
|
138
|
+
if (val = fetch(NO_VALUE)) == NO_VALUE
|
139
|
+
val = 'unset'
|
140
|
+
else
|
141
|
+
val = "value=#{val.is_a?(String) ? ('"' + val + '"') : val }"
|
142
|
+
end
|
143
|
+
"#<#{self.class} #{val}>"
|
144
|
+
end
|
145
|
+
|
146
|
+
# Describe the contents of this object in a string.
|
147
|
+
#
|
148
|
+
# @return [String] the string representation of this object
|
149
|
+
#
|
150
|
+
# @!visibility private
|
151
|
+
def to_s
|
152
|
+
value.to_s
|
153
|
+
end
|
154
|
+
|
155
|
+
private
|
156
|
+
|
157
|
+
# Checks the set status without locking the mutex.
|
158
|
+
# @return [Boolean] true when set else false
|
159
|
+
def has_been_set?
|
160
|
+
@value != NO_VALUE
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
data/lib/functional/memo.rb
CHANGED
@@ -15,14 +15,18 @@ module Functional
|
|
15
15
|
# @note Memoized method calls are thread safe and can safely be used in concurrent systems.
|
16
16
|
# Declaring memoization on a function is *not* thread safe and should only be done during
|
17
17
|
# application initialization.
|
18
|
+
#
|
19
|
+
# @since 1.0.0
|
18
20
|
module Memo
|
19
21
|
|
22
|
+
# @!visibility private
|
20
23
|
def self.extended(base)
|
21
24
|
base.extend(ClassMethods)
|
22
25
|
base.send(:__method_memos__=, {})
|
23
26
|
super(base)
|
24
27
|
end
|
25
28
|
|
29
|
+
# @!visibility private
|
26
30
|
def self.included(base)
|
27
31
|
base.extend(ClassMethods)
|
28
32
|
base.send(:__method_memos__=, {})
|
data/lib/functional/option.rb
CHANGED
@@ -17,6 +17,8 @@ module Functional
|
|
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
|
+
#
|
20
22
|
# @!macro thread_safe_immutable_object
|
21
23
|
class Option
|
22
24
|
include AbstractStruct
|
data/lib/functional/protocol.rb
CHANGED
@@ -4,6 +4,8 @@ module Functional
|
|
4
4
|
# itself from a block. Used by {Functional#SpecifyProtocol}.
|
5
5
|
#
|
6
6
|
# @see Functional::Protocol
|
7
|
+
#
|
8
|
+
# @since 1.0.0
|
7
9
|
class ProtocolInfo
|
8
10
|
|
9
11
|
# The symbolic name of the protocol
|
@@ -63,6 +65,7 @@ module Functional
|
|
63
65
|
private
|
64
66
|
|
65
67
|
# Data structure for encapsulating the protocol info data.
|
68
|
+
# @!visibility private
|
66
69
|
Info = Struct.new(:instance_methods, :class_methods, :constants)
|
67
70
|
|
68
71
|
# Does the target satisfy the constants expected by this protocol?
|
data/lib/functional/record.rb
CHANGED
@@ -19,6 +19,8 @@ module Functional
|
|
19
19
|
# @see Functional::AbstractStruct
|
20
20
|
# @see Functional::Union
|
21
21
|
#
|
22
|
+
# @since 1.0.0
|
23
|
+
#
|
22
24
|
# @!macro thread_safe_immutable_object
|
23
25
|
module Record
|
24
26
|
extend self
|
@@ -127,8 +129,8 @@ module Functional
|
|
127
129
|
# @return [Functional::AbstractStruct] the record class
|
128
130
|
def define_initializer(record)
|
129
131
|
record.send(:define_method, :initialize) do |data = {}|
|
130
|
-
restrictions =
|
131
|
-
data = fields.reduce({}) do |memo, field|
|
132
|
+
restrictions = record.class_variable_get(:@@restrictions)
|
133
|
+
data = record.fields.reduce({}) do |memo, field|
|
132
134
|
memo[field] = data.fetch(field, restrictions.clone_default(field))
|
133
135
|
memo
|
134
136
|
end
|
@@ -0,0 +1,247 @@
|
|
1
|
+
module Functional
|
2
|
+
|
3
|
+
# A tuple is a pure functional data strcture that is similar to an array but is
|
4
|
+
# immutable and of fixed length. Tuples support many of the same operations as
|
5
|
+
# array/list/vector.
|
6
|
+
#
|
7
|
+
# @note The current implementation uses simple Ruby arrays. This is likely to be
|
8
|
+
# very inefficient for all but the smallest tuples. The more items the tuple
|
9
|
+
# contains, the less efficient it will become. A future version will use a fast,
|
10
|
+
# immutable, persistent data structure such as a finger tree or a trie.
|
11
|
+
#
|
12
|
+
# @since 1.1.0
|
13
|
+
#
|
14
|
+
# @see http://en.wikipedia.org/wiki/Tuple
|
15
|
+
# @see http://msdn.microsoft.com/en-us/library/system.tuple.aspx
|
16
|
+
# @see http://www.tutorialspoint.com/python/python_tuples.htm
|
17
|
+
# @see http://en.cppreference.com/w/cpp/utility/tuple
|
18
|
+
# @see http://docs.oracle.com/javaee/6/api/javax/persistence/Tuple.html
|
19
|
+
# @see http://www.erlang.org/doc/reference_manual/data_types.html
|
20
|
+
# @see http://www.erlang.org/doc/man/erlang.html#make_tuple-2
|
21
|
+
# @see http://en.wikibooks.org/wiki/Haskell/Lists_and_tuples#Tuples
|
22
|
+
#
|
23
|
+
# @!macro thread_safe_immutable_object
|
24
|
+
class Tuple
|
25
|
+
|
26
|
+
# Create a new tuple with the given data items in the given order.
|
27
|
+
#
|
28
|
+
# @param [Array] data the data items to insert into the new tuple
|
29
|
+
# @raise [ArgumentError] if data is not an array or does not implement `to_a`
|
30
|
+
def initialize(data = [])
|
31
|
+
raise ArgumentError.new('data is not an array') unless data.respond_to?(:to_a)
|
32
|
+
@data = data.to_a.dup.freeze
|
33
|
+
self.freeze
|
34
|
+
end
|
35
|
+
|
36
|
+
# Retrieve the item at the given index. Indices begin at zero and increment
|
37
|
+
# up, just like Ruby arrays. Negative indicies begin at -1, which represents the
|
38
|
+
# last item in the tuple, and decrement toward the first item. If the
|
39
|
+
# given index is out of range then `nil` is returned.
|
40
|
+
#
|
41
|
+
# @param [Fixnum] index the index of the item to be retrieved
|
42
|
+
# @return [Object] the item at the given index or nil when index is out of bounds
|
43
|
+
def at(index)
|
44
|
+
@data[index]
|
45
|
+
end
|
46
|
+
alias_method :nth, :at
|
47
|
+
alias_method :[], :at
|
48
|
+
|
49
|
+
# Retrieve the item at the given index or return the given default value if the
|
50
|
+
# index is out of bounds. The behavior of indicies follows the rules for the
|
51
|
+
# `at` method.
|
52
|
+
#
|
53
|
+
# @param [Fixnum] index the index of the item to be retrieved
|
54
|
+
# @param [Object] default the value to return when given an out of bounds index
|
55
|
+
# @return [Object] the item at the given index or default when index is out of bounds
|
56
|
+
#
|
57
|
+
# @see Functional::Tuple#at
|
58
|
+
def fetch(index, default)
|
59
|
+
if index >= length || -index > length
|
60
|
+
default
|
61
|
+
else
|
62
|
+
at(index)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# The number of items in the tuple.
|
67
|
+
#
|
68
|
+
# @return [Fixnum] the number of items in the tuple
|
69
|
+
def length
|
70
|
+
@data.length
|
71
|
+
end
|
72
|
+
alias_method :size, :length
|
73
|
+
|
74
|
+
# Returns a new tuple containing elements common to the two tuples, excluding any
|
75
|
+
# duplicates. The order is preserved from the original tuple.
|
76
|
+
#
|
77
|
+
# @!macro [attach] tuple_method_param_other_return_tuple
|
78
|
+
# @param [Array] other the tuple or array-like object (responds to `to_a`) to operate on
|
79
|
+
# @return [Functional::Tuple] a new tuple with the appropriate items
|
80
|
+
def intersect(other)
|
81
|
+
Tuple.new(@data & other.to_a)
|
82
|
+
end
|
83
|
+
alias_method :&, :intersect
|
84
|
+
|
85
|
+
# Returns a new tuple by joining self with other, excluding any duplicates and
|
86
|
+
# preserving the order from the original tuple.
|
87
|
+
#
|
88
|
+
# @!macro tuple_method_param_other_return_tuple
|
89
|
+
def union(other)
|
90
|
+
Tuple.new(@data | other.to_a)
|
91
|
+
end
|
92
|
+
alias_method :|, :union
|
93
|
+
|
94
|
+
# Returns a new tuple built by concatenating the two tuples
|
95
|
+
# together to produce a third tuple.
|
96
|
+
#
|
97
|
+
# @!macro tuple_method_param_other_return_tuple
|
98
|
+
def concat(other)
|
99
|
+
Tuple.new(@data + other.to_a)
|
100
|
+
end
|
101
|
+
alias_method :+, :concat
|
102
|
+
|
103
|
+
# Returns a new tuple that is a copy of the original tuple, removing any items that
|
104
|
+
# also appear in other. The order is preserved from the original tuple.
|
105
|
+
#
|
106
|
+
# @!macro tuple_method_param_other_return_tuple
|
107
|
+
def diff(other)
|
108
|
+
Tuple.new(@data - other.to_a)
|
109
|
+
end
|
110
|
+
alias_method :-, :diff
|
111
|
+
|
112
|
+
# Returns a new tuple built by concatenating the given number of copies of self.
|
113
|
+
# Returns an empty tuple when the multiple is zero.
|
114
|
+
#
|
115
|
+
# @param [Fixnum] multiple the number of times to concatenate self
|
116
|
+
# @return [Functional::Tuple] a new tuple with the appropriate items
|
117
|
+
# @raise [ArgumentError] when multiple is a negative number
|
118
|
+
def repeat(multiple)
|
119
|
+
multiple = multiple.to_i
|
120
|
+
raise ArgumentError.new('negative argument') if multiple < 0
|
121
|
+
Tuple.new(@data * multiple)
|
122
|
+
end
|
123
|
+
alias_method :*, :repeat
|
124
|
+
|
125
|
+
# Returns a new tuple by removing duplicate values in self.
|
126
|
+
#
|
127
|
+
# @return [Functional::Tuple] the new tuple with only unique items
|
128
|
+
def uniq
|
129
|
+
Tuple.new(@data.uniq)
|
130
|
+
end
|
131
|
+
|
132
|
+
# Calls the given block once for each element in self, passing that element as a parameter.
|
133
|
+
# An Enumerator is returned if no block is given.
|
134
|
+
#
|
135
|
+
# @yieldparam [Object] item the current item
|
136
|
+
# @return [Enumerable] when no block is given
|
137
|
+
def each
|
138
|
+
return enum_for(:each) unless block_given?
|
139
|
+
@data.each do |item|
|
140
|
+
yield(item)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
# Calls the given block once for each element in self, passing that element
|
145
|
+
# and the current index as parameters. An Enumerator is returned if no block is given.
|
146
|
+
#
|
147
|
+
# @yieldparam [Object] item the current item
|
148
|
+
# @yieldparam [Fixnum] index the index of the current item
|
149
|
+
# @return [Enumerable] when no block is given
|
150
|
+
def each_with_index
|
151
|
+
return enum_for(:each_with_index) unless block_given?
|
152
|
+
@data.each_with_index do |item, index|
|
153
|
+
yield(item, index)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
# Calls the given block once for each element in self, passing that element
|
158
|
+
# and a tuple with all the remaining items in the tuple. When the last item
|
159
|
+
# is reached ab empty tuple is passed as the second parameter. This is the
|
160
|
+
# classic functional programming `head|tail` list processing idiom.
|
161
|
+
# An Enumerator is returned if no block is given.
|
162
|
+
#
|
163
|
+
# @yieldparam [Object] head the current item for this iteration
|
164
|
+
# @yieldparam [Tuple] tail the remaining items (tail) or an empty tuple when
|
165
|
+
# processing the last item
|
166
|
+
# @return [Enumerable] when no block is given
|
167
|
+
def sequence
|
168
|
+
return enum_for(:sequence) unless block_given?
|
169
|
+
@data.length.times do |index|
|
170
|
+
last = @data.length - 1
|
171
|
+
if index == last
|
172
|
+
yield(@data[index], Tuple.new)
|
173
|
+
else
|
174
|
+
yield(@data[index], Tuple.new(@data.slice(index+1..last)))
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
# Compares this object and other for equality. A tuple is `eql?` to
|
180
|
+
# other when other is a tuple or an array-like object (any object that
|
181
|
+
# responds to `to_a`) and the two objects have identical values in the
|
182
|
+
# same foxed order.
|
183
|
+
#
|
184
|
+
# @param [Object] other the other tuple to compare for equality
|
185
|
+
# @return [Boolean] true when equal else false
|
186
|
+
def eql?(other)
|
187
|
+
@data == other.to_a
|
188
|
+
end
|
189
|
+
alias_method :==, :eql?
|
190
|
+
|
191
|
+
# Returns true if self contains no items.
|
192
|
+
#
|
193
|
+
# @return [Boolean] true when empty else false
|
194
|
+
def empty?
|
195
|
+
@data.empty?
|
196
|
+
end
|
197
|
+
|
198
|
+
# Returns the first element of the tuple or nil when empty.
|
199
|
+
#
|
200
|
+
# @return [Object] the first element or nil
|
201
|
+
def first
|
202
|
+
@data.first
|
203
|
+
end
|
204
|
+
alias_method :head, :first
|
205
|
+
|
206
|
+
# Returns a tuple containing all the items in self after the first
|
207
|
+
# item. Returns an empty tuple when empty or there is only one item.
|
208
|
+
#
|
209
|
+
# @return [Functional::Tuple] the tail of the tuple
|
210
|
+
def rest
|
211
|
+
if @data.length <= 1
|
212
|
+
Tuple.new
|
213
|
+
else
|
214
|
+
Tuple.new(@data.slice(1..@data.length-1))
|
215
|
+
end
|
216
|
+
end
|
217
|
+
alias_method :tail, :rest
|
218
|
+
|
219
|
+
# Create a standard Ruby mutable array containing the tuple items
|
220
|
+
# in the same order.
|
221
|
+
#
|
222
|
+
# @return [Array] the new array created from the tuple
|
223
|
+
def to_a
|
224
|
+
@data.dup
|
225
|
+
end
|
226
|
+
alias_method :to_ary, :to_a
|
227
|
+
|
228
|
+
# Describe the contents of this object in a string.
|
229
|
+
#
|
230
|
+
# @return [String] the string representation of this object
|
231
|
+
#
|
232
|
+
# @!visibility private
|
233
|
+
def inspect
|
234
|
+
"#<#{self.class}: #{@data.to_s}>"
|
235
|
+
end
|
236
|
+
|
237
|
+
# Describe the contents of this object in a string that exactly
|
238
|
+
# matches the string that would be created from an identical array.
|
239
|
+
#
|
240
|
+
# @return [String] the string representation of this object
|
241
|
+
#
|
242
|
+
# @!visibility private
|
243
|
+
def to_s
|
244
|
+
@data.to_s
|
245
|
+
end
|
246
|
+
end
|
247
|
+
end
|