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