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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c56d810dbee5d5fb2e4b26daef765a75700d3e4f
|
4
|
+
data.tar.gz: e3e27de5c017c07eef5c9d78c590e73251fa98a9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 84ba63762cb021ceb483e8f175a23c9ea11c12b5fa66764662670b9a688d4d2a0f15bb7baed3b8e2b2207a5b37613b62dc1c90c647bbb68176c0208b41c7cf8e
|
7
|
+
data.tar.gz: 7411ea271610c82bbf180e28797ab0e0a0795200d7c77f252e435e098acfe7224236fd59700b72fad726d4a03d43fd1829277f75b14dd8f7f8afd615a44410e0
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
## Current Release v1.1.0 (August 12, 2014)
|
2
|
+
|
3
|
+
* A simple implementation of [tuple](http://en.wikipedia.org/wiki/Tuple), an
|
4
|
+
immutable, fixed-length list/array/vector-like data structure.
|
5
|
+
* `FinalStruct`, a variation on Ruby's `OpenStruct` in which all fields are "final" (meaning
|
6
|
+
that new fields can be arbitrarily added but once set each field becomes immutable).
|
7
|
+
* `FinalVar`, a thread safe object that holds a single value and is "final" (meaning
|
8
|
+
that the value can be set at most once after which it becomes immutable).
|
9
|
+
|
10
|
+
### Previous Release v1.0.0 (July 30, 2014)
|
11
|
+
|
12
|
+
* Protocol specifications inspired by Clojure [protocol](http://clojure.org/protocols),
|
13
|
+
Erlang [behavior](http://www.erlang.org/doc/design_principles/des_princ.html#id60128),
|
14
|
+
and Objective-C [protocol](https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/WorkingwithProtocols/WorkingwithProtocols.html)
|
15
|
+
* Function overloading with Erlang-style [function](http://erlang.org/doc/reference_manual/functions.html)
|
16
|
+
[pattern matching](http://erlang.org/doc/reference_manual/patterns.html)
|
17
|
+
* Simple, immutable data structures, such as *record* and *union*, inspired by
|
18
|
+
[Clojure](http://clojure.org/datatypes), [Erlang](http://www.erlang.org/doc/reference_manual/records.html),
|
19
|
+
and [others](http://en.wikipedia.org/wiki/Union_type)
|
20
|
+
* `Either` and `Option` classes based on [Functional Java](http://functionaljava.org/) and [Haskell](https://hackage.haskell.org/package/base-4.2.0.1/docs/Data-Either.html)
|
21
|
+
* [Memoization](http://en.wikipedia.org/wiki/Memoization) of class methods based on Clojure [memoize](http://clojuredocs.org/clojure_core/clojure.core/memoize)
|
22
|
+
* Lazy execution with a `Delay` class based on Clojure [delay](http://clojuredocs.org/clojure_core/clojure.core/delay)
|
data/README.md
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
# Functional Ruby
|
2
|
-
[](http://badge.fury.io/rb/functional-ruby) [](https://travis-ci.org/jdantonio/functional-ruby?branch=master) [](https://coveralls.io/r/jdantonio/functional-ruby) [](https://codeclimate.com/github/jdantonio/functional-ruby) [](http://inch-ci.org/github/jdantonio/functional-ruby) [](https://gemnasium.com/jdantonio/functional-ruby) [](http://opensource.org/licenses/MIT)
|
3
3
|
|
4
4
|
**A gem for adding functional programming tools to Ruby. Inspired by [Erlang](http://www.erlang.org/),
|
5
5
|
[Clojure](http://clojure.org/), and [Functional Java](http://functionaljava.org/).**
|
@@ -8,6 +8,8 @@
|
|
8
8
|
focus. Version 1.0 is a cohesive set of utilities inspired by other languages
|
9
9
|
but designed to work together in ways idiomatic to Ruby.*
|
10
10
|
|
11
|
+
Please see the [changelog](https://github.com/jdantonio/functional-ruby/blob/master/CHANGELOG.md) for more information.
|
12
|
+
|
11
13
|
## Introduction
|
12
14
|
|
13
15
|
Two things I love are [Ruby](http://www.ruby-lang.org/en/) and
|
@@ -38,15 +40,17 @@ Complete API documentation can be found at [Rubydoc.info](http://rubydoc.info/gi
|
|
38
40
|
|
39
41
|
* Protocol specifications inspired by Clojure [protocol](http://clojure.org/protocols),
|
40
42
|
Erlang [behavior](http://www.erlang.org/doc/design_principles/des_princ.html#id60128),
|
41
|
-
and Objective-C [protocol](https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/WorkingwithProtocols/WorkingwithProtocols.html)
|
43
|
+
and Objective-C [protocol](https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/WorkingwithProtocols/WorkingwithProtocols.html).
|
42
44
|
* Function overloading with Erlang-style [function](http://erlang.org/doc/reference_manual/functions.html)
|
43
|
-
[pattern matching](http://erlang.org/doc/reference_manual/patterns.html)
|
44
|
-
* Simple, immutable data structures, such as
|
45
|
+
[pattern matching](http://erlang.org/doc/reference_manual/patterns.html).
|
46
|
+
* Simple, thread safe, immutable data structures, such as `Record`, `Union`, and `Tuple`, inspired by
|
45
47
|
[Clojure](http://clojure.org/datatypes), [Erlang](http://www.erlang.org/doc/reference_manual/records.html),
|
46
|
-
and
|
47
|
-
* `Either` and `Option` classes based on [Functional Java](http://functionaljava.org/) and [Haskell](https://hackage.haskell.org/package/base-4.2.0.1/docs/Data-Either.html)
|
48
|
-
* [Memoization](http://en.wikipedia.org/wiki/Memoization) of class methods based on Clojure [memoize](http://clojuredocs.org/clojure_core/clojure.core/memoize)
|
49
|
-
* Lazy execution with a `Delay` class based on Clojure [delay](http://clojuredocs.org/clojure_core/clojure.core/delay)
|
48
|
+
and other functional languages.
|
49
|
+
* Thread safe, immutable `Either` and `Option` classes based on [Functional Java](http://functionaljava.org/) and [Haskell](https://hackage.haskell.org/package/base-4.2.0.1/docs/Data-Either.html).
|
50
|
+
* [Memoization](http://en.wikipedia.org/wiki/Memoization) of class methods based on Clojure [memoize](http://clojuredocs.org/clojure_core/clojure.core/memoize).
|
51
|
+
* Lazy execution with a `Delay` class based on Clojure [delay](http://clojuredocs.org/clojure_core/clojure.core/delay).
|
52
|
+
* Thread safe data structures, such as `FinalStruct` and `FinalVar`, which can be written to at most once
|
53
|
+
before becoming immutable. Based on [Java's `final` keyword](http://en.wikipedia.org/wiki/Final_(Java)).
|
50
54
|
|
51
55
|
### Supported Ruby Versions
|
52
56
|
|
@@ -75,7 +79,7 @@ require 'functional'
|
|
75
79
|
|
76
80
|
## Examples
|
77
81
|
|
78
|
-
Specifying a protocol:
|
82
|
+
Specifying a [protocol](http://rubydoc.info/github/jdantonio/functional-ruby/master/Functional/Protocol):
|
79
83
|
|
80
84
|
```ruby
|
81
85
|
Functional::SpecifyProtocol(:Name) do
|
@@ -86,7 +90,27 @@ Functional::SpecifyProtocol(:Name) do
|
|
86
90
|
end
|
87
91
|
```
|
88
92
|
|
89
|
-
|
93
|
+
Defining immutable [data structures](http://rubydoc.info/github/jdantonio/functional-ruby/master/Functional/AbstractStruct) including
|
94
|
+
[Either](http://rubydoc.info/github/jdantonio/functional-ruby/master/Functional/Either),
|
95
|
+
[Option](http://rubydoc.info/github/jdantonio/functional-ruby/master/Functional/Option),
|
96
|
+
[Union](http://rubydoc.info/github/jdantonio/functional-ruby/master/Functional/Union) and
|
97
|
+
[Record](http://rubydoc.info/github/jdantonio/functional-ruby/master/Functional/Record)
|
98
|
+
|
99
|
+
```ruby
|
100
|
+
Name = Functional::Record.new(:first, :middle, :last, :suffix) do
|
101
|
+
mandatory :first, :last
|
102
|
+
default :first, 'J.'
|
103
|
+
default :last, 'Doe'
|
104
|
+
end
|
105
|
+
|
106
|
+
anon = Name.new #=> #<record Name :first=>"J.", :middle=>nil, :last=>"Doe", :suffix=>nil>
|
107
|
+
matz = Name.new(first: 'Yukihiro', last: 'Matsumoto') #=> #<record Name :first=>"Yukihiro", :middle=>nil, :last=>"Matsumoto", :suffix=>nil>
|
108
|
+
```
|
109
|
+
|
110
|
+
[Pattern matching](http://rubydoc.info/github/jdantonio/functional-ruby/master/Functional/PatternMatching)
|
111
|
+
using [protocols](http://rubydoc.info/github/jdantonio/functional-ruby/master/Functional/Protocol),
|
112
|
+
[type](http://rubydoc.info/github/jdantonio/functional-ruby/master/Functional/TypeCheck) checking,
|
113
|
+
and other options:
|
90
114
|
|
91
115
|
```ruby
|
92
116
|
class Foo
|
@@ -110,12 +134,8 @@ class Foo
|
|
110
134
|
"Hello, #{name.first} #{name.last}!"
|
111
135
|
}.when {|name| Satisfy?(:Name) }
|
112
136
|
|
113
|
-
defn(:greet, :
|
114
|
-
"Hello,
|
115
|
-
}
|
116
|
-
|
117
|
-
defn(:greet, :female, _) { |name|
|
118
|
-
"Hello, Ms. #{name}!"
|
137
|
+
defn(:greet, :doctor, _) { |name|
|
138
|
+
"Hello, Dr. #{name}!"
|
119
139
|
}
|
120
140
|
|
121
141
|
defn(:greet, nil, _) { |name|
|
@@ -128,7 +148,7 @@ class Foo
|
|
128
148
|
end
|
129
149
|
```
|
130
150
|
|
131
|
-
|
151
|
+
Performance improvement of idempotent functions through [memoization](http://rubydoc.info/github/jdantonio/functional-ruby/master/Functional/Memo):
|
132
152
|
|
133
153
|
```ruby
|
134
154
|
class Factors
|
data/doc/record.txt
CHANGED
@@ -124,6 +124,69 @@
|
|
124
124
|
# is created. Default values should be simple types like `String`, `Fixnum`,
|
125
125
|
# and `Boolean`. If complex operations need performed when setting default
|
126
126
|
# values the a `Class` should be used instead of a `Record`.
|
127
|
+
#
|
128
|
+
# ### Why Declaration Differs from Ruby's Struct
|
129
|
+
#
|
130
|
+
# Those familiar with Ruby's `Struct` class will notice one important
|
131
|
+
# difference when declaring a `Record`: the block passes to `new` cannot be
|
132
|
+
# used to define additional methods. When declaring a new class created from a
|
133
|
+
# Ruby `Struct` the block can perform any additional class definition that
|
134
|
+
# could be done had the class be defined normally. The excellent
|
135
|
+
# [Values](https://github.com/tcrayford/Values) supports this same behavior.
|
136
|
+
# `Record` does not allow additional class definitions during declaration for
|
137
|
+
# one simple reason: doing so violates two very important tenets of functional
|
138
|
+
# programming. Specifically, immutability and the separation of data from
|
139
|
+
# operations.
|
140
|
+
#
|
141
|
+
# `Record` exists for the purpose of creating immutable objects. If additional
|
142
|
+
# instance methods were to be defined on a record class it would be possible
|
143
|
+
# to violate immutability. Not only could additional, mutable state be added
|
144
|
+
# to the class, but the existing immutable attributes could be overridden by
|
145
|
+
# mutable methods. The security of providing an immutable object would be
|
146
|
+
# completely shattered, thus defeating the original purpose of the record
|
147
|
+
# class. Of course it would be possible to allow this feature and trust the
|
148
|
+
# programmer to not violate the intended immutability of class, but opening
|
149
|
+
# `Record` to the *possibility* of immutability violation is unnecessary and
|
150
|
+
# unwise.
|
151
|
+
#
|
152
|
+
# More important than the potential for immutability violations is the fact
|
153
|
+
# the adding additional methods to a record violates the principal of
|
154
|
+
# separating data from operations on that data. This is one of the core ideas
|
155
|
+
# in functional programming. Data is defined in pure structures that contain
|
156
|
+
# no behavior and operations on that data are provided by polymorphic
|
157
|
+
# functions. This may seem counterintuitive to object oriented programmers,
|
158
|
+
# but that is the nature of functional programming. Adding behavior to a
|
159
|
+
# record, even when that behavior does not violate immutability, is still
|
160
|
+
# anathema to functional programming, and it is why records in languages like
|
161
|
+
# Erlang and Clojure do not have functions defined within them.
|
162
|
+
#
|
163
|
+
# Should additional methods need defined on a `Record` class, the appropriate
|
164
|
+
# practice is to declare the record class then declare another class which
|
165
|
+
# extends the record. The record class remains pure data and the subclass
|
166
|
+
# contains additional operations on that data.
|
167
|
+
#
|
168
|
+
# ```ruby
|
169
|
+
# NameRecord = Functional::Record.new(:first, :middle, :last, :suffix) do
|
170
|
+
# mandatory :first, :last
|
171
|
+
# end
|
172
|
+
#
|
173
|
+
# class Name < NameRecord
|
174
|
+
# def full_name
|
175
|
+
# "#{first} #{last}"
|
176
|
+
# end
|
177
|
+
#
|
178
|
+
# def formal_name
|
179
|
+
# name = [first, middle, last].select{|s| ! s.to_s.empty?}.join(' ')
|
180
|
+
# suffix.to_s.empty? ? name : name + ", #{suffix}"
|
181
|
+
# end
|
182
|
+
# end
|
183
|
+
#
|
184
|
+
# jerry = Name.new(first: 'Jerry', last: "D'Antonio")
|
185
|
+
# ted = Name.new(first: 'Ted', middle: 'Theodore', last: 'Logan', suffix: 'Esq.')
|
186
|
+
#
|
187
|
+
# jerry.formal_name #=> "Jerry D'Antonio"
|
188
|
+
# ted.formal_name #=> "Ted Theodore Logan, Esq."
|
189
|
+
# ```
|
127
190
|
#
|
128
191
|
# ## Inspiration
|
129
192
|
#
|
data/doc/thread_safety.txt
CHANGED
@@ -6,3 +6,12 @@
|
|
6
6
|
# mutable references to mutable objects. This cannot be changed. The best
|
7
7
|
# practice it to only encapsulate immutable, frozen, or thread safe objects.
|
8
8
|
# Ultimately, thread safety is the responsibility of the programmer.
|
9
|
+
#
|
10
|
+
# @!macro [new] thread_safe_final_object
|
11
|
+
#
|
12
|
+
# @note This is a write-once, read-many, thread safe object that can
|
13
|
+
# be used in concurrent systems. Thread safety guarantees *cannot* be made
|
14
|
+
# about objects contained *within* this object, however. Ruby variables are
|
15
|
+
# mutable references to mutable objects. This cannot be changed. The best
|
16
|
+
# practice it to only encapsulate immutable, frozen, or thread safe objects.
|
17
|
+
# Ultimately, thread safety is the responsibility of the programmer.
|
data/lib/functional.rb
CHANGED
@@ -1,13 +1,17 @@
|
|
1
1
|
require 'functional/delay'
|
2
2
|
require 'functional/either'
|
3
|
+
require 'functional/final_struct'
|
4
|
+
require 'functional/final_var'
|
3
5
|
require 'functional/memo'
|
4
6
|
require 'functional/option'
|
5
7
|
require 'functional/pattern_matching'
|
6
8
|
require 'functional/protocol'
|
7
9
|
require 'functional/protocol_info'
|
8
10
|
require 'functional/record'
|
11
|
+
require 'functional/tuple'
|
9
12
|
require 'functional/type_check'
|
10
13
|
require 'functional/union'
|
14
|
+
require 'functional/value_struct'
|
11
15
|
require 'functional/version'
|
12
16
|
|
13
17
|
Functional::SpecifyProtocol(:Disposition) do
|
@@ -11,6 +11,8 @@ end
|
|
11
11
|
module Functional
|
12
12
|
|
13
13
|
# An abstract base class for immutable struct classes.
|
14
|
+
#
|
15
|
+
# @since 1.0.0
|
14
16
|
module AbstractStruct
|
15
17
|
|
16
18
|
# @return [Array] the values of all record fields in order, frozen
|
@@ -47,7 +49,7 @@ module Functional
|
|
47
49
|
# field values (according to `Object#==`).
|
48
50
|
#
|
49
51
|
# @param [Object] other the other record to compare for equality
|
50
|
-
# @return [
|
52
|
+
# @return [Boolean] true when equal else false
|
51
53
|
def eql?(other)
|
52
54
|
self.class == other.class && self.to_h == other.to_h
|
53
55
|
end
|
@@ -55,7 +57,7 @@ module Functional
|
|
55
57
|
|
56
58
|
# @!macro [attach] inspect_method
|
57
59
|
#
|
58
|
-
# Describe the contents of this
|
60
|
+
# Describe the contents of this struct in a string. Will include the name of the
|
59
61
|
# record class, all fields, and all values.
|
60
62
|
#
|
61
63
|
# @return [String] the class and contents of this record
|
data/lib/functional/delay.rb
CHANGED
data/lib/functional/either.rb
CHANGED
@@ -77,6 +77,8 @@ module Functional
|
|
77
77
|
# @see https://hackage.haskell.org/package/base-4.2.0.1/docs/Data-Either.html Haskell Data.Either
|
78
78
|
# @see http://ruby-concurrency.github.io/concurrent-ruby/Concurrent/Obligation.html Concurrent Ruby
|
79
79
|
#
|
80
|
+
# @since 1.0.0
|
81
|
+
#
|
80
82
|
# @!macro thread_safe_immutable_object
|
81
83
|
class Either
|
82
84
|
include AbstractStruct
|
@@ -0,0 +1,231 @@
|
|
1
|
+
require_relative 'final_var'
|
2
|
+
require 'thread'
|
3
|
+
|
4
|
+
module Functional
|
5
|
+
|
6
|
+
# A variation on Ruby's `OpenStruct` in which all fields are "final" (meaning
|
7
|
+
# that new fields can be arbitrarily added to a `FinalStruct` object but once
|
8
|
+
# set each field becomes immutable). Additionally, predicate methods exist for
|
9
|
+
# all fields and these predicates indicate if the field has been set.
|
10
|
+
#
|
11
|
+
# There are two ways to initialize a `FinalStruct`: with zero arguments or
|
12
|
+
# with a `Hash` (or any other object that implements a `to_h` method). The
|
13
|
+
# only difference in behavior is that a `FinalStruct` initialized with a
|
14
|
+
# hash will pre-define and pre-populate attributes named for the hash keys
|
15
|
+
# and with values corresponding to the hash values.
|
16
|
+
#
|
17
|
+
# @example Instanciation With No Fields
|
18
|
+
# bucket = Functional::FinalStruct.new
|
19
|
+
#
|
20
|
+
# bucket.foo #=> nil
|
21
|
+
# bucket.foo? #=> false
|
22
|
+
#
|
23
|
+
# bucket.foo = 42 #=> 42
|
24
|
+
# bucket.foo #=> 42
|
25
|
+
# bucket.foo? #=> true
|
26
|
+
#
|
27
|
+
# bucket.foo = 42 #=> Functional::FinalityError: final accessor 'bar' has already been set
|
28
|
+
#
|
29
|
+
# @example Instanciation With a Hash
|
30
|
+
# name = Functional::FinalStruct.new(first: 'Douglas', last: 'Adams')
|
31
|
+
#
|
32
|
+
# name.first #=> 'Douglas'
|
33
|
+
# name.last #=> 'Adams'
|
34
|
+
# name.first? #=> true
|
35
|
+
# name.last? #=> true
|
36
|
+
#
|
37
|
+
# name.middle #=> nil
|
38
|
+
# name.middle? #=> false
|
39
|
+
# name.middle = 'Noel' #=> 'Noel'
|
40
|
+
# name.middle? #=> true
|
41
|
+
#
|
42
|
+
# name.first = 'Sam' #=> Functional::FinalityError: final accessor 'first' has already been set
|
43
|
+
#
|
44
|
+
# @since 1.1.0
|
45
|
+
#
|
46
|
+
# @see Functional::FinalVar
|
47
|
+
# @see http://www.ruby-doc.org/stdlib-2.1.2/libdoc/ostruct/rdoc/OpenStruct.html
|
48
|
+
# @see http://en.wikipedia.org/wiki/Final_(Java) Java `final` keyword
|
49
|
+
#
|
50
|
+
# @!macro thread_safe_final_object
|
51
|
+
class FinalStruct
|
52
|
+
|
53
|
+
# Creates a new `FinalStruct` object. By default, the resulting `FinalStruct`
|
54
|
+
# object will have no attributes. The optional hash, if given, will generate
|
55
|
+
# attributes and values (can be a `Hash` or any object with a `to_h` method).
|
56
|
+
#
|
57
|
+
# @param [Hash] attributes the field/value pairs to set on creation
|
58
|
+
def initialize(attributes = {})
|
59
|
+
raise ArgumentError.new('attributes must be given as a hash or not at all') unless attributes.respond_to?(:to_h)
|
60
|
+
@mutex = Mutex.new
|
61
|
+
@attribute_hash = {}
|
62
|
+
attributes.to_h.each_pair do |field, value|
|
63
|
+
set_attribute(field, value)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# @!macro [attach] final_struct_get_method
|
68
|
+
#
|
69
|
+
# Get the value of the given field.
|
70
|
+
#
|
71
|
+
# @param [Symbol] field the field to retrieve the value for
|
72
|
+
# @return [Object] the value of the field is set else nil
|
73
|
+
def get(field)
|
74
|
+
@mutex.synchronize {
|
75
|
+
get_attribute(field)
|
76
|
+
}
|
77
|
+
end
|
78
|
+
alias_method :[], :get
|
79
|
+
|
80
|
+
# @!macro [attach] final_struct_set_method
|
81
|
+
#
|
82
|
+
# Set the value of the give field to the given value.
|
83
|
+
#
|
84
|
+
# It is a logical error to attempt to set a `final` field more than once, as this
|
85
|
+
# violates the concept of finality. Calling the method a second or subsequent time
|
86
|
+
# for a given field will result in an exception being raised.
|
87
|
+
#
|
88
|
+
# @param [Symbol] field the field to set the value for
|
89
|
+
# @param [Object] value the value to set the field to
|
90
|
+
# @return [Object] the final value of the given field
|
91
|
+
#
|
92
|
+
# @raise [Functional::FinalityError] if the given field has already been set
|
93
|
+
def set(field, value)
|
94
|
+
@mutex.synchronize {
|
95
|
+
if attribute_has_been_set?(field)
|
96
|
+
raise FinalityError.new("final accessor '#{field}' has already been set")
|
97
|
+
else
|
98
|
+
set_attribute(field, value)
|
99
|
+
end
|
100
|
+
}
|
101
|
+
end
|
102
|
+
alias_method :[]=, :set
|
103
|
+
|
104
|
+
# @!macro [attach] final_struct_set_predicate
|
105
|
+
#
|
106
|
+
# Check the internal hash to unambiguously verify that the given
|
107
|
+
# attribute has been set.
|
108
|
+
#
|
109
|
+
# @param [Symbol] field the field to get the value for
|
110
|
+
# @return [Boolean] true if the field has been set else false
|
111
|
+
def set?(field)
|
112
|
+
@mutex.synchronize {
|
113
|
+
attribute_has_been_set?(field)
|
114
|
+
}
|
115
|
+
end
|
116
|
+
|
117
|
+
# Get the current value of the given field if already set else set the value of
|
118
|
+
# the given field to the given value.
|
119
|
+
#
|
120
|
+
# @param [Symbol] field the field to get or set the value for
|
121
|
+
# @param [Object] value the value to set the field to when not previously set
|
122
|
+
# @return [Object] the final value of the given field
|
123
|
+
def get_or_set(field, value)
|
124
|
+
@mutex.synchronize {
|
125
|
+
attribute_has_been_set?(field) ? get_attribute(field) : set_attribute(field, value)
|
126
|
+
}
|
127
|
+
end
|
128
|
+
|
129
|
+
# Get the current value of the given field if already set else return the given
|
130
|
+
# default value.
|
131
|
+
#
|
132
|
+
# @param [Symbol] field the field to get the value for
|
133
|
+
# @param [Object] default the value to return if the field has not been set
|
134
|
+
# @return [Object] the value of the given field else the given default value
|
135
|
+
def fetch(field, default)
|
136
|
+
@mutex.synchronize {
|
137
|
+
attribute_has_been_set?(field) ? get_attribute(field) : default
|
138
|
+
}
|
139
|
+
end
|
140
|
+
|
141
|
+
# Calls the block once for each attribute, passing the key/value pair as parameters.
|
142
|
+
# If no block is given, an enumerator is returned instead.
|
143
|
+
#
|
144
|
+
# @yieldparam [Symbol] field the struct field for the current iteration
|
145
|
+
# @yieldparam [Object] value the value of the current field
|
146
|
+
#
|
147
|
+
# @return [Enumerable] when no block is given
|
148
|
+
def each_pair
|
149
|
+
return enum_for(:each_pair) unless block_given?
|
150
|
+
@mutex.synchronize {
|
151
|
+
@attribute_hash.each do |field, value|
|
152
|
+
yield(field, value)
|
153
|
+
end
|
154
|
+
}
|
155
|
+
end
|
156
|
+
|
157
|
+
# Converts the `FinalStruct` to a `Hash` with keys representing each attribute
|
158
|
+
# (as symbols) and their corresponding values.
|
159
|
+
#
|
160
|
+
# @return [Hash] a `Hash` representing this struct
|
161
|
+
def to_h
|
162
|
+
@mutex.synchronize {
|
163
|
+
@attribute_hash.dup
|
164
|
+
}
|
165
|
+
end
|
166
|
+
|
167
|
+
# Compares this object and other for equality. A `FinalStruct` is `eql?` to
|
168
|
+
# other when other is a `FinalStruct` and the two objects have identical
|
169
|
+
# fields and values.
|
170
|
+
#
|
171
|
+
# @param [Object] other the other record to compare for equality
|
172
|
+
# @return [Boolean] true when equal else false
|
173
|
+
def eql?(other)
|
174
|
+
other.is_a?(self.class) && to_h == other.to_h
|
175
|
+
end
|
176
|
+
alias_method :==, :eql?
|
177
|
+
|
178
|
+
# Describe the contents of this object in a string.
|
179
|
+
#
|
180
|
+
# @return [String] the string representation of this object
|
181
|
+
#
|
182
|
+
# @!visibility private
|
183
|
+
def inspect
|
184
|
+
state = to_h.to_s.gsub(/^{/, '').gsub(/}$/, '')
|
185
|
+
"#<#{self.class} #{state}>"
|
186
|
+
end
|
187
|
+
alias_method :to_s, :inspect
|
188
|
+
|
189
|
+
protected
|
190
|
+
|
191
|
+
# @!macro final_struct_get_method
|
192
|
+
# @!visibility private
|
193
|
+
def get_attribute(field)
|
194
|
+
@attribute_hash[field.to_sym]
|
195
|
+
end
|
196
|
+
|
197
|
+
# @!macro final_struct_set_method
|
198
|
+
# @!visibility private
|
199
|
+
def set_attribute(field, value)
|
200
|
+
@attribute_hash[field.to_sym] = value
|
201
|
+
end
|
202
|
+
|
203
|
+
# @!macro final_struct_set_predicate
|
204
|
+
# @!visibility private
|
205
|
+
def attribute_has_been_set?(field)
|
206
|
+
@attribute_hash.has_key?(field.to_sym)
|
207
|
+
end
|
208
|
+
|
209
|
+
# Check the method name and args for signatures matching potential
|
210
|
+
# final attribute reader, writer, and predicate methods. If the signature
|
211
|
+
# matches a reader or predicate, treat the attribute as unset. If the
|
212
|
+
# signature matches a writer, attempt to set the new attribute.
|
213
|
+
#
|
214
|
+
# @param [Symbol] symbol the name of the called function
|
215
|
+
# @param [Array] args zero or more arguments
|
216
|
+
# @return [Object] the result of the proxied method or the `super` call
|
217
|
+
#
|
218
|
+
# @!visibility private
|
219
|
+
def method_missing(symbol, *args)
|
220
|
+
if args.length == 1 && (match = /([^=]+)=$/.match(symbol))
|
221
|
+
set(match[1], args.first)
|
222
|
+
elsif args.length == 0 && (match = /([^\?]+)\?$/.match(symbol))
|
223
|
+
set?(match[1])
|
224
|
+
elsif args.length == 0
|
225
|
+
get(symbol)
|
226
|
+
else
|
227
|
+
super
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|