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
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
|
-
[![Gem Version](https://badge.fury.io/rb/functional-ruby.
|
2
|
+
[![Gem Version](https://badge.fury.io/rb/functional-ruby.svg)](http://badge.fury.io/rb/functional-ruby) [![Build Status](https://secure.travis-ci.org/jdantonio/functional-ruby.png)](https://travis-ci.org/jdantonio/functional-ruby?branch=master) [![Coverage Status](https://coveralls.io/repos/jdantonio/functional-ruby/badge.png)](https://coveralls.io/r/jdantonio/functional-ruby) [![Code Climate](https://codeclimate.com/github/jdantonio/functional-ruby.png)](https://codeclimate.com/github/jdantonio/functional-ruby) [![Inline docs](http://inch-ci.org/github/jdantonio/functional-ruby.png)](http://inch-ci.org/github/jdantonio/functional-ruby) [![Dependency Status](https://gemnasium.com/jdantonio/functional-ruby.png)](https://gemnasium.com/jdantonio/functional-ruby) [![License](http://img.shields.io/license/MIT.png?color=green)](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
|