functional-ruby 1.1.0 → 1.2.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 +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
|