functional-ruby 1.1.0 → 1.2.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 +4 -0
- data/README.md +14 -12
- data/doc/memo.md +192 -0
- data/doc/pattern_matching.md +481 -0
- data/doc/protocol.md +219 -0
- data/doc/record.md +247 -0
- data/lib/functional/abstract_struct.rb +8 -8
- data/lib/functional/delay.rb +31 -38
- data/lib/functional/either.rb +48 -45
- data/lib/functional/final_struct.rb +23 -34
- data/lib/functional/final_var.rb +20 -21
- data/lib/functional/memo.rb +33 -24
- data/lib/functional/method_signature.rb +1 -2
- data/lib/functional/option.rb +7 -7
- data/lib/functional/pattern_matching.rb +12 -10
- data/lib/functional/protocol.rb +2 -4
- data/lib/functional/protocol_info.rb +5 -3
- data/lib/functional/record.rb +82 -16
- data/lib/functional/synchronization.rb +88 -0
- data/lib/functional/tuple.rb +14 -4
- data/lib/functional/type_check.rb +0 -2
- data/lib/functional/union.rb +5 -4
- data/lib/functional/value_struct.rb +5 -3
- data/lib/functional/version.rb +1 -1
- data/spec/functional/complex_pattern_matching_spec.rb +1 -2
- data/spec/functional/configuration_spec.rb +0 -2
- data/spec/functional/delay_spec.rb +0 -2
- data/spec/functional/either_spec.rb +0 -1
- data/spec/functional/final_struct_spec.rb +0 -1
- data/spec/functional/final_var_spec.rb +0 -2
- data/spec/functional/memo_spec.rb +7 -10
- data/spec/functional/option_spec.rb +0 -1
- data/spec/functional/pattern_matching_spec.rb +0 -1
- data/spec/functional/protocol_info_spec.rb +0 -2
- data/spec/functional/protocol_spec.rb +1 -3
- data/spec/functional/record_spec.rb +170 -87
- data/spec/functional/tuple_spec.rb +0 -1
- data/spec/functional/type_check_spec.rb +0 -2
- data/spec/functional/union_spec.rb +0 -1
- data/spec/functional/value_struct_spec.rb +0 -1
- metadata +14 -29
- data/doc/memo.txt +0 -192
- data/doc/pattern_matching.txt +0 -485
- data/doc/protocol.txt +0 -221
- data/doc/record.txt +0 -207
- data/doc/thread_safety.txt +0 -17
data/lib/functional/final_var.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
require '
|
1
|
+
require 'functional/synchronization'
|
2
2
|
|
3
3
|
module Functional
|
4
4
|
|
@@ -30,15 +30,20 @@ module Functional
|
|
30
30
|
# f.set? #=> true
|
31
31
|
# f.value #=> 42
|
32
32
|
#
|
33
|
-
# @since 1.1.0
|
34
|
-
#
|
35
33
|
# @see Functional::FinalStruct
|
36
34
|
# @see http://en.wikipedia.org/wiki/Final_(Java) Java `final` keyword
|
37
35
|
#
|
38
|
-
# @!macro thread_safe_final_object
|
39
|
-
|
36
|
+
# @!macro [new] thread_safe_final_object
|
37
|
+
#
|
38
|
+
# @note This is a write-once, read-many, thread safe object that can
|
39
|
+
# be used in concurrent systems. Thread safety guarantees *cannot* be made
|
40
|
+
# about objects contained *within* this object, however. Ruby variables are
|
41
|
+
# mutable references to mutable objects. This cannot be changed. The best
|
42
|
+
# practice it to only encapsulate immutable, frozen, or thread safe objects.
|
43
|
+
# Ultimately, thread safety is the responsibility of the programmer.
|
44
|
+
class FinalVar < Synchronization::Object
|
40
45
|
|
41
|
-
# @!visibility private
|
46
|
+
# @!visibility private
|
42
47
|
NO_VALUE = Object.new.freeze
|
43
48
|
|
44
49
|
# Create a new `FinalVar` with the given value or "unset" when
|
@@ -46,17 +51,15 @@ module Functional
|
|
46
51
|
#
|
47
52
|
# @param [Object] value if given, the immutable value of the object
|
48
53
|
def initialize(value = NO_VALUE)
|
49
|
-
|
50
|
-
@value = value
|
54
|
+
super
|
55
|
+
synchronize{ @value = value }
|
51
56
|
end
|
52
57
|
|
53
58
|
# Get the current value or nil if unset.
|
54
59
|
#
|
55
60
|
# @return [Object] the current value or nil
|
56
61
|
def get
|
57
|
-
|
58
|
-
has_been_set? ? @value : nil
|
59
|
-
}
|
62
|
+
synchronize { has_been_set? ? @value : nil }
|
60
63
|
end
|
61
64
|
alias_method :value, :get
|
62
65
|
|
@@ -66,13 +69,13 @@ module Functional
|
|
66
69
|
# @return [Object] the new value
|
67
70
|
# @raise [Functional::FinalityError] if the value has already been set
|
68
71
|
def set(value)
|
69
|
-
|
72
|
+
synchronize do
|
70
73
|
if has_been_set?
|
71
74
|
raise FinalityError.new('value has already been set')
|
72
75
|
else
|
73
76
|
@value = value
|
74
77
|
end
|
75
|
-
|
78
|
+
end
|
76
79
|
end
|
77
80
|
alias_method :value=, :set
|
78
81
|
|
@@ -80,9 +83,7 @@ module Functional
|
|
80
83
|
#
|
81
84
|
# @return [Boolean] true when the value has been set else false
|
82
85
|
def set?
|
83
|
-
|
84
|
-
has_been_set?
|
85
|
-
}
|
86
|
+
synchronize { has_been_set? }
|
86
87
|
end
|
87
88
|
alias_method :value?, :set?
|
88
89
|
|
@@ -91,13 +92,13 @@ module Functional
|
|
91
92
|
# @param [Object] value the value to set
|
92
93
|
# @return [Object] the current value if already set else the new value
|
93
94
|
def get_or_set(value)
|
94
|
-
|
95
|
+
synchronize do
|
95
96
|
if has_been_set?
|
96
97
|
@value
|
97
98
|
else
|
98
99
|
@value = value
|
99
100
|
end
|
100
|
-
|
101
|
+
end
|
101
102
|
end
|
102
103
|
|
103
104
|
# Get the value if set else return the given default value.
|
@@ -105,9 +106,7 @@ module Functional
|
|
105
106
|
# @param [Object] default the value to return if currently unset
|
106
107
|
# @return [Object] the current value when set else the given default
|
107
108
|
def fetch(default)
|
108
|
-
|
109
|
-
has_been_set? ? @value : default
|
110
|
-
}
|
109
|
+
synchronize { has_been_set? ? @value : default }
|
111
110
|
end
|
112
111
|
|
113
112
|
# Compares this object and other for equality. A `FinalVar` that is unset
|
data/lib/functional/memo.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
require '
|
1
|
+
require 'functional/synchronization'
|
2
2
|
|
3
3
|
module Functional
|
4
4
|
|
@@ -9,14 +9,12 @@ module Functional
|
|
9
9
|
# the cached result. As a result the response time for frequently called
|
10
10
|
# functions is vastly incresed (after the first call with any given set of)
|
11
11
|
# arguments, at the cost of increased memory usage (the cache).
|
12
|
-
#
|
13
|
-
# @!macro memoize
|
14
12
|
#
|
15
|
-
#
|
16
|
-
# Declaring memoization on a function is *not* thread safe and should only be done during
|
17
|
-
# application initialization.
|
13
|
+
# {include:file:doc/memo.md}
|
18
14
|
#
|
19
|
-
# @
|
15
|
+
# @note Memoized method calls are thread safe and can safely be used in
|
16
|
+
# concurrent systems. Declaring memoization on a function is *not* thread
|
17
|
+
# safe and should only be done during application initialization.
|
20
18
|
module Memo
|
21
19
|
|
22
20
|
# @!visibility private
|
@@ -37,24 +35,36 @@ module Functional
|
|
37
35
|
module ClassMethods
|
38
36
|
|
39
37
|
# @!visibility private
|
40
|
-
|
38
|
+
class Memoizer < Synchronization::Object
|
39
|
+
attr_reader :function, :cache, :max_cache
|
40
|
+
def initialize(function, max_cache)
|
41
|
+
super
|
42
|
+
synchronize do
|
43
|
+
@function = function
|
44
|
+
@max_cache = max_cache
|
45
|
+
@cache = {}
|
46
|
+
end
|
47
|
+
end
|
41
48
|
def max_cache?
|
42
49
|
max_cache > 0 && cache.size >= max_cache
|
43
50
|
end
|
51
|
+
public :synchronize
|
44
52
|
end
|
53
|
+
private_constant :Memoizer
|
45
54
|
|
46
55
|
# @!visibility private
|
47
56
|
attr_accessor :__method_memos__
|
48
57
|
|
49
58
|
# Returns a memoized version of a referentially transparent function. The
|
50
|
-
# memoized version of the function keeps a cache of the mapping from
|
51
|
-
# to results and, when calls with the same arguments are
|
52
|
-
# higher performance at the expense of higher memory
|
59
|
+
# memoized version of the function keeps a cache of the mapping from
|
60
|
+
# arguments to results and, when calls with the same arguments are
|
61
|
+
# repeated often, has higher performance at the expense of higher memory
|
62
|
+
# use.
|
53
63
|
#
|
54
64
|
# @param [Symbol] func the class/module function to memoize
|
55
65
|
# @param [Hash] opts the options controlling memoization
|
56
|
-
# @option opts [Fixnum] :at_most the maximum number of memos to store in
|
57
|
-
# cache; a value of zero (the default) or `nil` indicates no limit
|
66
|
+
# @option opts [Fixnum] :at_most the maximum number of memos to store in
|
67
|
+
# the cache; a value of zero (the default) or `nil` indicates no limit
|
58
68
|
#
|
59
69
|
# @raise [ArgumentError] when the method has already been memoized
|
60
70
|
# @raise [ArgumentError] when :at_most option is a negative number
|
@@ -63,7 +73,7 @@ module Functional
|
|
63
73
|
max_cache = opts[:at_most].to_i
|
64
74
|
raise ArgumentError.new("method :#{func} has already been memoized") if __method_memos__.has_key?(func)
|
65
75
|
raise ArgumentError.new(':max_cache must be > 0') if max_cache < 0
|
66
|
-
__method_memos__[func] =
|
76
|
+
__method_memos__[func] = Memoizer.new(method(func), max_cache.to_i)
|
67
77
|
__define_memo_proxy__(func)
|
68
78
|
nil
|
69
79
|
end
|
@@ -80,17 +90,16 @@ module Functional
|
|
80
90
|
# @!visibility private
|
81
91
|
def __proxy_memoized_method__(func, *args, &block)
|
82
92
|
memo = self.__method_memos__[func]
|
83
|
-
memo.
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
93
|
+
memo.synchronize do
|
94
|
+
if block_given?
|
95
|
+
memo.function.call(*args, &block)
|
96
|
+
elsif memo.cache.has_key?(args)
|
97
|
+
memo.cache[args]
|
98
|
+
else
|
99
|
+
result = memo.function.call(*args)
|
100
|
+
memo.cache[args] = result unless memo.max_cache?
|
101
|
+
end
|
91
102
|
end
|
92
|
-
ensure
|
93
|
-
memo.mutex.unlock
|
94
103
|
end
|
95
104
|
end
|
96
105
|
end
|
@@ -6,8 +6,6 @@ module Functional
|
|
6
6
|
#
|
7
7
|
# Helper functions used when pattern matching runtime arguments against
|
8
8
|
# a method defined with the `defn` function of Functional::PatternMatching.
|
9
|
-
#
|
10
|
-
# @since 1.0.0
|
11
9
|
module MethodSignature
|
12
10
|
extend self
|
13
11
|
|
@@ -70,5 +68,6 @@ module Functional
|
|
70
68
|
param == PatternMatching::UNBOUND || param == arg
|
71
69
|
end
|
72
70
|
end
|
71
|
+
private_constant :MethodSignature
|
73
72
|
end
|
74
73
|
end
|
data/lib/functional/option.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
1
|
+
require 'functional/abstract_struct'
|
2
|
+
require 'functional/either'
|
3
|
+
require 'functional/protocol'
|
4
|
+
require 'functional/synchronization'
|
4
5
|
|
5
6
|
Functional::SpecifyProtocol(:Option) do
|
6
7
|
instance_method :some?, 0
|
@@ -14,13 +15,10 @@ module Functional
|
|
14
15
|
# This type is a replacement for the use of nil with better type checks.
|
15
16
|
# It is an immutable data structure that extends `AbstractStruct`.
|
16
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
|
-
#
|
22
20
|
# @!macro thread_safe_immutable_object
|
23
|
-
class Option
|
21
|
+
class Option < Synchronization::Object
|
24
22
|
include AbstractStruct
|
25
23
|
|
26
24
|
# @!visibility private
|
@@ -201,11 +199,13 @@ module Functional
|
|
201
199
|
#
|
202
200
|
# @!visibility private
|
203
201
|
def initialize(value, none, reason = nil)
|
202
|
+
super
|
204
203
|
@none = none
|
205
204
|
@reason = none ? reason : nil
|
206
205
|
hsh = none ? {some: nil} : {some: value}
|
207
206
|
set_data_hash(hsh)
|
208
207
|
set_values_array(hsh.values)
|
208
|
+
ensure_ivar_visibility!
|
209
209
|
end
|
210
210
|
end
|
211
211
|
end
|
@@ -1,17 +1,17 @@
|
|
1
|
-
|
1
|
+
require 'functional/method_signature'
|
2
2
|
|
3
3
|
module Functional
|
4
4
|
|
5
|
-
# As much as I love Ruby I've always been a little disappointed that Ruby
|
6
|
-
# support function overloading. Function overloading tends to reduce
|
7
|
-
# and keep function signatures simpler. No sweat, I learned to do
|
8
|
-
# I started programming in Erlang. My favorite Erlang feature
|
9
|
-
# question, pattern matching. Pattern matching is like function
|
10
|
-
# cranked to 11. So one day I was musing on Twitter that I'd like
|
11
|
-
# Erlang-stype pattern matching in Ruby and one of my friends responded
|
5
|
+
# As much as I love Ruby I've always been a little disappointed that Ruby
|
6
|
+
# doesn't support function overloading. Function overloading tends to reduce
|
7
|
+
# branching and keep function signatures simpler. No sweat, I learned to do
|
8
|
+
# without. Then I started programming in Erlang. My favorite Erlang feature
|
9
|
+
# is, without question, pattern matching. Pattern matching is like function
|
10
|
+
# overloading cranked to 11. So one day I was musing on Twitter that I'd like
|
11
|
+
# to see Erlang-stype pattern matching in Ruby and one of my friends responded
|
12
12
|
# "Build it!" So I did. And here it is.
|
13
13
|
#
|
14
|
-
#
|
14
|
+
# {include:file:doc/pattern_matching.md}
|
15
15
|
module PatternMatching
|
16
16
|
|
17
17
|
# A parameter that is required but that can take any value.
|
@@ -40,9 +40,11 @@ module Functional
|
|
40
40
|
self
|
41
41
|
end
|
42
42
|
end
|
43
|
+
private_constant :GuardClause
|
43
44
|
|
44
45
|
# @!visibility private
|
45
46
|
FunctionPattern = Struct.new(:function, :args, :body, :guard)
|
47
|
+
private_constant :FunctionPattern
|
46
48
|
|
47
49
|
# @!visibility private
|
48
50
|
def __unbound_args__(match, args)
|
@@ -55,7 +57,7 @@ module Functional
|
|
55
57
|
argv << args[i][key] if value == UNBOUND
|
56
58
|
end
|
57
59
|
elsif p.is_a?(Hash) || p == UNBOUND || p.is_a?(Class)
|
58
|
-
argv << args[i]
|
60
|
+
argv << args[i]
|
59
61
|
end
|
60
62
|
end
|
61
63
|
argv
|
data/lib/functional/protocol.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
|
1
|
+
require 'functional/protocol_info'
|
2
2
|
|
3
3
|
module Functional
|
4
4
|
|
@@ -55,10 +55,8 @@ module Functional
|
|
55
55
|
# interrogate a module, class, or object for its type and ancestry, protocols
|
56
56
|
# allow modules, classes, and methods to be interrogated based on their behavior.
|
57
57
|
# It is a logical extension of the `respond_to?` method, but vastly more powerful.
|
58
|
-
#
|
59
|
-
# @!macro protocol
|
60
58
|
#
|
61
|
-
#
|
59
|
+
# {include:file:doc/protocol.md}
|
62
60
|
module Protocol
|
63
61
|
|
64
62
|
# The global registry of specified protocols.
|
@@ -1,12 +1,12 @@
|
|
1
|
+
require 'functional/synchronization'
|
2
|
+
|
1
3
|
module Functional
|
2
4
|
|
3
5
|
# An immutable object describing a single protocol and capable of building
|
4
6
|
# itself from a block. Used by {Functional#SpecifyProtocol}.
|
5
7
|
#
|
6
8
|
# @see Functional::Protocol
|
7
|
-
|
8
|
-
# @since 1.0.0
|
9
|
-
class ProtocolInfo
|
9
|
+
class ProtocolInfo < Synchronization::Object
|
10
10
|
|
11
11
|
# The symbolic name of the protocol
|
12
12
|
attr_reader :name
|
@@ -22,11 +22,13 @@ module Functional
|
|
22
22
|
def initialize(name, &specification)
|
23
23
|
raise ArgumentError.new('no block given') unless block_given?
|
24
24
|
raise ArgumentError.new('no name given') if name.nil? || name.empty?
|
25
|
+
super
|
25
26
|
@name = name.to_sym
|
26
27
|
@info = Info.new({}, {}, [])
|
27
28
|
self.instance_eval(&specification)
|
28
29
|
@info.each_pair{|col, _| col.freeze}
|
29
30
|
@info.freeze
|
31
|
+
ensure_ivar_visibility!
|
30
32
|
self.freeze
|
31
33
|
end
|
32
34
|
|
data/lib/functional/record.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
require 'functional/abstract_struct'
|
2
|
+
require 'functional/protocol'
|
3
|
+
require 'functional/type_check'
|
3
4
|
|
4
5
|
module Functional
|
5
6
|
|
@@ -8,18 +9,17 @@ module Functional
|
|
8
9
|
# using accessor methods, without having to write an explicit class.
|
9
10
|
# The `Record` module generates new `AbstractStruct` subclasses that hold a
|
10
11
|
# set of fields with a reader method for each field.
|
11
|
-
#
|
12
|
+
#
|
12
13
|
# A `Record` is very similar to a Ruby `Struct` and shares many of its behaviors
|
13
14
|
# and attributes. Unlike a # Ruby `Struct`, a `Record` is immutable: its values
|
14
15
|
# are set at construction and can never be changed. Divergence between the two
|
15
16
|
# classes derive from this core difference.
|
16
|
-
#
|
17
|
-
# @!macro record
|
18
17
|
#
|
19
|
-
#
|
20
|
-
# @see Functional::Union
|
18
|
+
# {include:file:doc/record.md}
|
21
19
|
#
|
22
|
-
# @
|
20
|
+
# @see Functional::Union
|
21
|
+
# @see Functional::Protocol
|
22
|
+
# @see Functional::TypeCheck
|
23
23
|
#
|
24
24
|
# @!macro thread_safe_immutable_object
|
25
25
|
module Record
|
@@ -28,10 +28,30 @@ module Functional
|
|
28
28
|
# Create a new record class with the given fields.
|
29
29
|
#
|
30
30
|
# @return [Functional::AbstractStruct] the new record subclass
|
31
|
-
# @raise [ArgumentError] no fields specified
|
31
|
+
# @raise [ArgumentError] no fields specified or an invalid type
|
32
|
+
# specification is given
|
32
33
|
def new(*fields, &block)
|
33
34
|
raise ArgumentError.new('no fields provided') if fields.empty?
|
34
|
-
|
35
|
+
|
36
|
+
name = nil
|
37
|
+
types = nil
|
38
|
+
|
39
|
+
# check if a name for registration is given
|
40
|
+
if fields.first.is_a?(String)
|
41
|
+
name = fields.first
|
42
|
+
fields = fields[1..fields.length-1]
|
43
|
+
end
|
44
|
+
|
45
|
+
# check for a set of type/protocol specifications
|
46
|
+
if fields.size == 1 && fields.first.respond_to?(:to_h)
|
47
|
+
types = fields.first
|
48
|
+
fields = fields.first.keys
|
49
|
+
check_types!(types)
|
50
|
+
end
|
51
|
+
|
52
|
+
build(name, fields, types, &block)
|
53
|
+
rescue
|
54
|
+
raise ArgumentError.new('invalid specification')
|
35
55
|
end
|
36
56
|
|
37
57
|
private
|
@@ -40,14 +60,18 @@ module Functional
|
|
40
60
|
#
|
41
61
|
# A set of restrictions governing the creation of a new record.
|
42
62
|
class Restrictions
|
63
|
+
include Protocol
|
43
64
|
include TypeCheck
|
44
65
|
|
45
66
|
# Create a new restrictions object by processing the given
|
46
67
|
# block. The block should be the DSL for defining a record class.
|
47
68
|
#
|
69
|
+
# @param [Hash] types a hash of fields and the associated type/protocol
|
70
|
+
# when type/protocol checking is among the restrictions
|
48
71
|
# @param [Proc] block A DSL definition of a new record.
|
49
72
|
# @yield A DSL definition of a new record.
|
50
|
-
def initialize(&block)
|
73
|
+
def initialize(types = nil, &block)
|
74
|
+
@types = types
|
51
75
|
@required = []
|
52
76
|
@defaults = {}
|
53
77
|
instance_eval(&block) if block_given?
|
@@ -86,18 +110,44 @@ module Functional
|
|
86
110
|
return value
|
87
111
|
end
|
88
112
|
|
113
|
+
# Validate the record data against this set of restrictions.
|
114
|
+
#
|
115
|
+
# @param [Hash] data the data hash
|
116
|
+
# @raise [ArgumentError] when the data does not match the restrictions
|
117
|
+
def validate!(data)
|
118
|
+
validate_mandatory!(data)
|
119
|
+
validate_types!(data)
|
120
|
+
end
|
121
|
+
|
122
|
+
private
|
123
|
+
|
89
124
|
# Check the given data hash to see if it contains non-nil values for
|
90
125
|
# all mandatory fields.
|
91
126
|
#
|
92
127
|
# @param [Hash] data the data hash
|
93
128
|
# @raise [ArgumentError] if any mandatory fields are missing
|
94
|
-
def
|
129
|
+
def validate_mandatory!(data)
|
95
130
|
if data.any?{|k,v| @required.include?(k) && v.nil? }
|
96
131
|
raise ArgumentError.new('mandatory fields must not be nil')
|
97
132
|
end
|
98
133
|
end
|
99
134
|
|
100
|
-
|
135
|
+
# Validate the record data against a type/protocol specification.
|
136
|
+
#
|
137
|
+
# @param [Hash] data the data hash
|
138
|
+
# @raise [ArgumentError] when the data does not match the specification
|
139
|
+
def validate_types!(data)
|
140
|
+
return if @types.nil?
|
141
|
+
@types.each do |field, type|
|
142
|
+
value = data[field]
|
143
|
+
next if value.nil?
|
144
|
+
if type.is_a? Module
|
145
|
+
raise ArgumentError.new("'#{field}' must be of type #{type}") unless Type?(value, type)
|
146
|
+
else
|
147
|
+
raise ArgumentError.new("'#{field}' must stasify the protocol :#{type}") unless Satisfy?(value, type)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
101
151
|
|
102
152
|
# Is the given object uncloneable?
|
103
153
|
#
|
@@ -107,15 +157,29 @@ module Functional
|
|
107
157
|
Type? object, NilClass, TrueClass, FalseClass, Fixnum, Bignum, Float
|
108
158
|
end
|
109
159
|
end
|
160
|
+
private_constant :Restrictions
|
161
|
+
|
162
|
+
# Validate the given type/protocol specification.
|
163
|
+
#
|
164
|
+
# @param [Hash] types the type specification
|
165
|
+
# @raise [ArgumentError] when the specification is not valid
|
166
|
+
def check_types!(types)
|
167
|
+
return if types.nil?
|
168
|
+
unless types.all?{|k,v| v.is_a?(Module) || v.is_a?(Symbol) }
|
169
|
+
raise ArgumentError.new('invalid specification')
|
170
|
+
end
|
171
|
+
end
|
110
172
|
|
111
173
|
# Use the given `AbstractStruct` class and build the methods necessary
|
112
174
|
# to support the given data fields.
|
113
175
|
#
|
176
|
+
# @param [String] name the name under which to register the record when given
|
114
177
|
# @param [Array] fields the list of symbolic names for all data fields
|
115
178
|
# @return [Functional::AbstractStruct] the record class
|
116
|
-
def build(fields, &block)
|
179
|
+
def build(name, fields, types, &block)
|
180
|
+
fields = [name].concat(fields) unless name.nil?
|
117
181
|
record, fields = AbstractStruct.define_class(self, :record, fields)
|
118
|
-
record.class_variable_set(:@@restrictions, Restrictions.new(&block))
|
182
|
+
record.class_variable_set(:@@restrictions, Restrictions.new(types, &block))
|
119
183
|
define_initializer(record)
|
120
184
|
fields.each do |field|
|
121
185
|
define_reader(record, field)
|
@@ -129,14 +193,16 @@ module Functional
|
|
129
193
|
# @return [Functional::AbstractStruct] the record class
|
130
194
|
def define_initializer(record)
|
131
195
|
record.send(:define_method, :initialize) do |data = {}|
|
196
|
+
super()
|
132
197
|
restrictions = record.class_variable_get(:@@restrictions)
|
133
198
|
data = record.fields.reduce({}) do |memo, field|
|
134
199
|
memo[field] = data.fetch(field, restrictions.clone_default(field))
|
135
200
|
memo
|
136
201
|
end
|
137
|
-
restrictions.
|
202
|
+
restrictions.validate!(data)
|
138
203
|
set_data_hash(data)
|
139
204
|
set_values_array(data.values)
|
205
|
+
ensure_ivar_visibility!
|
140
206
|
self.freeze
|
141
207
|
end
|
142
208
|
record
|