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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: fe9f8b8bb0900055e86688c7995156bb89b5c0af
4
- data.tar.gz: 794627125c2647c8e813b873c69f9be62bf3f206
3
+ metadata.gz: c56d810dbee5d5fb2e4b26daef765a75700d3e4f
4
+ data.tar.gz: e3e27de5c017c07eef5c9d78c590e73251fa98a9
5
5
  SHA512:
6
- metadata.gz: b2e54bc22942b8e2be11ca19fe1c0bd393a1a868bff7f2717d5830efea7e048b53f574763352b778170fe6e46d76d7e42bd566200843595a97142a35572a7941
7
- data.tar.gz: 21c78e069e28438cc55dcdd3f0a2dc0892a5f36236337cd42a28dc8597d04b33f9565a3a46b03ce8c405b3b7b80ade995e2cd4f24c01642335382eaaaf917fc6
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.png)](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)
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 *record* and *union*, inspired by
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 [others](http://en.wikipedia.org/wiki/Union_type)
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
- Pattern matching using protocols, type checking, and other options:
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, :male, _) { |name|
114
- "Hello, Mr. #{name}!"
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
- Memoization:
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
  #
@@ -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 [Booleab] true when equal else false
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 record in a string. Will include the name of the
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
@@ -17,6 +17,8 @@ module Functional
17
17
  #
18
18
  # @see http://clojuredocs.org/clojure_core/clojure.core/delay Clojure delay
19
19
  #
20
+ # @since 1.0.0
21
+ #
20
22
  # @!macro thread_safe_immutable_object
21
23
  class Delay
22
24
 
@@ -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