functional-ruby 0.7.7 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +92 -152
- data/doc/memo.txt +192 -0
- data/doc/pattern_matching.txt +485 -0
- data/doc/protocol.txt +221 -0
- data/doc/record.txt +144 -0
- data/doc/thread_safety.txt +8 -0
- data/lib/functional.rb +48 -18
- data/lib/functional/abstract_struct.rb +161 -0
- data/lib/functional/delay.rb +117 -0
- data/lib/functional/either.rb +222 -0
- data/lib/functional/memo.rb +93 -0
- data/lib/functional/method_signature.rb +72 -0
- data/lib/functional/option.rb +209 -0
- data/lib/functional/pattern_matching.rb +117 -100
- data/lib/functional/protocol.rb +157 -0
- data/lib/functional/protocol_info.rb +193 -0
- data/lib/functional/record.rb +155 -0
- data/lib/functional/type_check.rb +112 -0
- data/lib/functional/union.rb +152 -0
- data/lib/functional/version.rb +3 -1
- data/spec/functional/abstract_struct_shared.rb +154 -0
- data/spec/functional/complex_pattern_matching_spec.rb +205 -0
- data/spec/functional/configuration_spec.rb +17 -0
- data/spec/functional/delay_spec.rb +147 -0
- data/spec/functional/either_spec.rb +237 -0
- data/spec/functional/memo_spec.rb +207 -0
- data/spec/functional/option_spec.rb +292 -0
- data/spec/functional/pattern_matching_spec.rb +279 -276
- data/spec/functional/protocol_info_spec.rb +444 -0
- data/spec/functional/protocol_spec.rb +274 -0
- data/spec/functional/record_spec.rb +175 -0
- data/spec/functional/type_check_spec.rb +103 -0
- data/spec/functional/union_spec.rb +110 -0
- data/spec/spec_helper.rb +6 -4
- metadata +55 -45
- data/lib/functional/behavior.rb +0 -138
- data/lib/functional/behaviour.rb +0 -2
- data/lib/functional/catalog.rb +0 -487
- data/lib/functional/collection.rb +0 -403
- data/lib/functional/inflect.rb +0 -127
- data/lib/functional/platform.rb +0 -120
- data/lib/functional/search.rb +0 -132
- data/lib/functional/sort.rb +0 -41
- data/lib/functional/utilities.rb +0 -189
- data/md/behavior.md +0 -188
- data/md/catalog.md +0 -32
- data/md/collection.md +0 -32
- data/md/inflect.md +0 -32
- data/md/pattern_matching.md +0 -512
- data/md/platform.md +0 -32
- data/md/search.md +0 -32
- data/md/sort.md +0 -32
- data/md/utilities.md +0 -55
- data/spec/functional/behavior_spec.rb +0 -528
- data/spec/functional/catalog_spec.rb +0 -1206
- data/spec/functional/collection_spec.rb +0 -752
- data/spec/functional/inflect_spec.rb +0 -85
- data/spec/functional/integration_spec.rb +0 -205
- data/spec/functional/platform_spec.rb +0 -501
- data/spec/functional/search_spec.rb +0 -187
- data/spec/functional/sort_spec.rb +0 -61
- data/spec/functional/utilities_spec.rb +0 -277
@@ -0,0 +1,161 @@
|
|
1
|
+
require_relative 'protocol'
|
2
|
+
|
3
|
+
Functional::SpecifyProtocol(:Struct) do
|
4
|
+
instance_method :fields
|
5
|
+
instance_method :values
|
6
|
+
instance_method :length
|
7
|
+
instance_method :each
|
8
|
+
instance_method :each_pair
|
9
|
+
end
|
10
|
+
|
11
|
+
module Functional
|
12
|
+
|
13
|
+
# An abstract base class for immutable struct classes.
|
14
|
+
module AbstractStruct
|
15
|
+
|
16
|
+
# @return [Array] the values of all record fields in order, frozen
|
17
|
+
attr_reader :values
|
18
|
+
|
19
|
+
# Yields the value of each record field in order.
|
20
|
+
# If no block is given an enumerator is returned.
|
21
|
+
#
|
22
|
+
# @yieldparam [Object] value the value of the given field
|
23
|
+
#
|
24
|
+
# @return [Enumerable] when no block is given
|
25
|
+
def each
|
26
|
+
return enum_for(:each) unless block_given?
|
27
|
+
fields.each do |field|
|
28
|
+
yield(self.send(field))
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Yields the name and value of each record field in order.
|
33
|
+
# If no block is given an enumerator is returned.
|
34
|
+
#
|
35
|
+
# @yieldparam [Symbol] field the record field for the current iteration
|
36
|
+
# @yieldparam [Object] value the value of the current field
|
37
|
+
#
|
38
|
+
# @return [Enumerable] when no block is given
|
39
|
+
def each_pair
|
40
|
+
return enum_for(:each_pair) unless block_given?
|
41
|
+
fields.each do |field|
|
42
|
+
yield(field, self.send(field))
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Equality--Returns `true` if `other` has the same record subclass and has equal
|
47
|
+
# field values (according to `Object#==`).
|
48
|
+
#
|
49
|
+
# @param [Object] other the other record to compare for equality
|
50
|
+
# @return [Booleab] true when equal else false
|
51
|
+
def eql?(other)
|
52
|
+
self.class == other.class && self.to_h == other.to_h
|
53
|
+
end
|
54
|
+
alias_method :==, :eql?
|
55
|
+
|
56
|
+
# @!macro [attach] inspect_method
|
57
|
+
#
|
58
|
+
# Describe the contents of this record in a string. Will include the name of the
|
59
|
+
# record class, all fields, and all values.
|
60
|
+
#
|
61
|
+
# @return [String] the class and contents of this record
|
62
|
+
def inspect
|
63
|
+
state = to_h.to_s.gsub(/^{/, '').gsub(/}$/, '')
|
64
|
+
"#<#{self.class.datatype} #{self.class} #{state}>"
|
65
|
+
end
|
66
|
+
alias_method :to_s, :inspect
|
67
|
+
|
68
|
+
# Returns the number of record fields.
|
69
|
+
#
|
70
|
+
# @return [Fixnum] the number of record fields
|
71
|
+
def length
|
72
|
+
fields.length
|
73
|
+
end
|
74
|
+
alias_method :size, :length
|
75
|
+
|
76
|
+
# A frozen array of all record fields.
|
77
|
+
#
|
78
|
+
# @return [Array] all record fields in order, frozen
|
79
|
+
def fields
|
80
|
+
self.class.fields
|
81
|
+
end
|
82
|
+
|
83
|
+
# Returns a Hash containing the names and values for the record’s fields.
|
84
|
+
#
|
85
|
+
# @return [Hash] collection of all fields and their associated values
|
86
|
+
def to_h
|
87
|
+
@data
|
88
|
+
end
|
89
|
+
|
90
|
+
protected
|
91
|
+
|
92
|
+
# Set the internal data hash to a copy of the given hash and freeze it.
|
93
|
+
# @param [Hash] data the data hash
|
94
|
+
#
|
95
|
+
# @!visibility private
|
96
|
+
def set_data_hash(data)
|
97
|
+
@data = data.dup.freeze
|
98
|
+
end
|
99
|
+
|
100
|
+
# Set the internal values array to a copy of the given array and freeze it.
|
101
|
+
# @param [Array] values the values array
|
102
|
+
#
|
103
|
+
# @!visibility private
|
104
|
+
def set_values_array(values)
|
105
|
+
@values = values.dup.freeze
|
106
|
+
end
|
107
|
+
|
108
|
+
# Define a new struct class and, if necessary, register it with
|
109
|
+
# the calling class/module. Will also set the datatype and fields
|
110
|
+
# class attributes on the new struct class.
|
111
|
+
#
|
112
|
+
# @param [Module] parent the class/module that is defining the new struct
|
113
|
+
# @param [Symbol] datatype the datatype value for the new struct class
|
114
|
+
# @param [Array] fields the list of symbolic names for all data fields
|
115
|
+
# @return [Functional::AbstractStruct, Array] the new class and the
|
116
|
+
# (possibly) updated fields array
|
117
|
+
#
|
118
|
+
# @!visibility private
|
119
|
+
def self.define_class(parent, datatype, fields)
|
120
|
+
struct = Class.new{ include AbstractStruct }
|
121
|
+
if fields.first.is_a? String
|
122
|
+
parent.const_set(fields.first, struct)
|
123
|
+
fields = fields[1, fields.length-1]
|
124
|
+
end
|
125
|
+
fields = fields.collect{|field| field.to_sym }.freeze
|
126
|
+
struct.send(:datatype=, datatype.to_sym)
|
127
|
+
struct.send(:fields=, fields)
|
128
|
+
[struct, fields]
|
129
|
+
end
|
130
|
+
|
131
|
+
private
|
132
|
+
|
133
|
+
def self.included(base)
|
134
|
+
base.extend(ClassMethods)
|
135
|
+
super(base)
|
136
|
+
end
|
137
|
+
|
138
|
+
# Class methods added to a class that includes {Functional::PatternMatching}
|
139
|
+
#
|
140
|
+
# @!visibility private
|
141
|
+
module ClassMethods
|
142
|
+
|
143
|
+
# A frozen Array of all record fields in order
|
144
|
+
attr_reader :fields
|
145
|
+
|
146
|
+
# A symbol describing the object's datatype
|
147
|
+
attr_reader :datatype
|
148
|
+
|
149
|
+
private
|
150
|
+
|
151
|
+
# A frozen Array of all record fields in order
|
152
|
+
attr_writer :fields
|
153
|
+
|
154
|
+
# A symbol describing the object's datatype
|
155
|
+
attr_writer :datatype
|
156
|
+
|
157
|
+
fields = [].freeze
|
158
|
+
datatype = :struct
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
require 'thread'
|
2
|
+
|
3
|
+
module Functional
|
4
|
+
|
5
|
+
# Lazy evaluation of a block yielding an immutable result. Useful for expensive
|
6
|
+
# operations that may never be needed.
|
7
|
+
#
|
8
|
+
# When a `Delay` is created its state is set to `pending`. The value and
|
9
|
+
# reason are both `nil`. The first time the `#value` method is called the
|
10
|
+
# enclosed opration will be run and the calling thread will block. Other
|
11
|
+
# threads attempting to call `#value` will block as well. Once the operation
|
12
|
+
# is complete the *value* will be set to the result of the operation or the
|
13
|
+
# *reason* will be set to the raised exception, as appropriate. All threads
|
14
|
+
# blocked on `#value` will return. Subsequent calls to `#value` will immediately
|
15
|
+
# return the cached value. The operation will only be run once. This means that
|
16
|
+
# any side effects created by the operation will only happen once as well.
|
17
|
+
#
|
18
|
+
# @see http://clojuredocs.org/clojure_core/clojure.core/delay Clojure delay
|
19
|
+
#
|
20
|
+
# @!macro thread_safe_immutable_object
|
21
|
+
class Delay
|
22
|
+
|
23
|
+
# Create a new `Delay` in the `:pending` state.
|
24
|
+
#
|
25
|
+
# @yield the delayed operation to perform
|
26
|
+
#
|
27
|
+
# @raise [ArgumentError] if no block is given
|
28
|
+
def initialize(&block)
|
29
|
+
raise ArgumentError.new('no block given') unless block_given?
|
30
|
+
@mutex = Mutex.new
|
31
|
+
@state = :pending
|
32
|
+
@task = block
|
33
|
+
end
|
34
|
+
|
35
|
+
# Current state of block processing.
|
36
|
+
#
|
37
|
+
# @return [Symbol] the current state of block processing
|
38
|
+
def state
|
39
|
+
@mutex.lock
|
40
|
+
@state
|
41
|
+
ensure
|
42
|
+
@mutex.unlock
|
43
|
+
end
|
44
|
+
|
45
|
+
# The exception raised when processing the block. Returns `nil` if the
|
46
|
+
# operation is still `:pending` or has been `:fulfilled`.
|
47
|
+
#
|
48
|
+
# @return [StandardError] the exception raised when processing the block else nil
|
49
|
+
def reason
|
50
|
+
@mutex.lock
|
51
|
+
@reason
|
52
|
+
ensure
|
53
|
+
@mutex.unlock
|
54
|
+
end
|
55
|
+
|
56
|
+
# Return the (possibly memoized) value of the delayed operation.
|
57
|
+
#
|
58
|
+
# If the state is `:pending` then the calling thread will block while the
|
59
|
+
# operation is performed. All other threads simultaneously calling `#value`
|
60
|
+
# will block as well. Once the operation is complete (either `:fulfilled` or
|
61
|
+
# `:rejected`) all waiting threads will unblock and the new value will be
|
62
|
+
# returned.
|
63
|
+
#
|
64
|
+
# If the state is not `:pending` when `#value` is called the (possibly memoized)
|
65
|
+
# value will be returned without blocking and without performing the operation
|
66
|
+
# again.
|
67
|
+
#
|
68
|
+
# @return [Object] the (possibly memoized) result of the block operation
|
69
|
+
def value
|
70
|
+
@mutex.lock
|
71
|
+
execute_task_once
|
72
|
+
@value
|
73
|
+
ensure
|
74
|
+
@mutex.unlock
|
75
|
+
end
|
76
|
+
|
77
|
+
# Has the delay been fulfilled?
|
78
|
+
# @return [Boolean]
|
79
|
+
def fulfilled?
|
80
|
+
state == :fulfilled
|
81
|
+
end
|
82
|
+
alias_method :value?, :fulfilled?
|
83
|
+
|
84
|
+
# Has the delay been rejected?
|
85
|
+
# @return [Boolean]
|
86
|
+
def rejected?
|
87
|
+
state == :rejected
|
88
|
+
end
|
89
|
+
alias_method :reason?, :rejected?
|
90
|
+
|
91
|
+
# Is delay completion still pending?
|
92
|
+
# @return [Boolean]
|
93
|
+
def pending?
|
94
|
+
state == :pending
|
95
|
+
end
|
96
|
+
|
97
|
+
protected
|
98
|
+
|
99
|
+
# @!visibility private
|
100
|
+
#
|
101
|
+
# Execute the enclosed task then cache and return the result if
|
102
|
+
# the current state is pending. Otherwise, return the cached result.
|
103
|
+
#
|
104
|
+
# @return [Object] the result of the block operation
|
105
|
+
def execute_task_once
|
106
|
+
if @state == :pending
|
107
|
+
begin
|
108
|
+
@value = @task.call
|
109
|
+
@state = :fulfilled
|
110
|
+
rescue => ex
|
111
|
+
@reason = ex
|
112
|
+
@state = :rejected
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,222 @@
|
|
1
|
+
require_relative 'abstract_struct'
|
2
|
+
require_relative 'protocol'
|
3
|
+
|
4
|
+
Functional::SpecifyProtocol(:Either) do
|
5
|
+
instance_method :left, 0
|
6
|
+
instance_method :left?, 0
|
7
|
+
instance_method :right, 0
|
8
|
+
instance_method :right?, 0
|
9
|
+
end
|
10
|
+
|
11
|
+
module Functional
|
12
|
+
|
13
|
+
# The `Either` type represents a value of one of two possible types (a disjoint union).
|
14
|
+
# It is an immutable structure that contains one and only one value. That value can
|
15
|
+
# be stored in one of two virtual position, `left` or `right`. The position provides
|
16
|
+
# context for the encapsulated data.
|
17
|
+
#
|
18
|
+
# One of the main uses of `Either` is as a return value that can indicate either
|
19
|
+
# success or failure. Object oriented programs generally report errors through
|
20
|
+
# either state or exception handling, neither of which work well in functional
|
21
|
+
# programming. In the former case, a method is called on an object and when an
|
22
|
+
# error occurs the state of the object is updated to reflect the error. This does
|
23
|
+
# not translate well to functional programming because they eschew state and
|
24
|
+
# mutable objects. In the latter, an exception handling block provides branching
|
25
|
+
# logic when an exception is thrown. This does not translate well to functional
|
26
|
+
# programming because it eschews side effects like structured exception handling
|
27
|
+
# (and structured exception handling tends to be very expensive). `Either` provides
|
28
|
+
# a powerful and easy-to-use alternative.
|
29
|
+
#
|
30
|
+
# A function that may generate an error can choose to return an immutable `Either`
|
31
|
+
# object in which the position of the value (left or right) indicates the nature
|
32
|
+
# of the data. By convention, a `left` value indicates an error and a `right` value
|
33
|
+
# indicates success. This leaves the caller with no ambiguity regarding success or
|
34
|
+
# failure, requires no persistent state, and does not require expensive exception
|
35
|
+
# handling facilities.
|
36
|
+
#
|
37
|
+
# `Either` provides several aliases and convenience functions to facilitate these
|
38
|
+
# failure/success conventions. The `left` and `right` functions, including their
|
39
|
+
# derivatives, are mirrored by `reason` and `value`. Failure is indicated by the
|
40
|
+
# presence of a `reason` and success is indicated by the presence of a `value`.
|
41
|
+
# When an operation has failed the either is in a `rejected` state, and when an
|
42
|
+
# operation has successed the either is in a `fulfilled` state. A common convention
|
43
|
+
# is to use a Ruby `Exception` as the `reason`. The factory method `error` facilitates
|
44
|
+
# this. The semantics and conventions of `reason`, `value`, and their derivatives
|
45
|
+
# follow the conventions of the Concurrent Ruby gem.
|
46
|
+
#
|
47
|
+
# The `left`/`right` and `reason`/`value` methods are not mutually exclusive. They
|
48
|
+
# can be commingled and still result in functionally correct code. This practice
|
49
|
+
# should be avoided, however. Consistent use of either `left`/`right` or
|
50
|
+
# `reason`/`value` against each `Either` instance will result in more expressive,
|
51
|
+
# intent-revealing code.
|
52
|
+
#
|
53
|
+
# @example
|
54
|
+
#
|
55
|
+
# require 'uri'
|
56
|
+
#
|
57
|
+
# def web_host(url)
|
58
|
+
# uri = URI(url)
|
59
|
+
# if uri.scheme == 'http'
|
60
|
+
# Functional::Either.left(uri.host)
|
61
|
+
# else
|
62
|
+
# Functional::Either.right('Invalid HTTP URL')
|
63
|
+
# end
|
64
|
+
# end
|
65
|
+
#
|
66
|
+
# good = web_host('http://www.concurrent-ruby.com')
|
67
|
+
# good.left? #=> true
|
68
|
+
# good.left #=> "www.concurrent-ruby"
|
69
|
+
# good.right #=> nil
|
70
|
+
#
|
71
|
+
# good = web_host('bogus')
|
72
|
+
# good.left? #=> false
|
73
|
+
# good.left #=> nil
|
74
|
+
# good.right #=> "Invalid HTTP URL"
|
75
|
+
#
|
76
|
+
# @see http://functionaljava.googlecode.com/svn/artifacts/3.0/javadoc/fj/data/Either.html Functional Java
|
77
|
+
# @see https://hackage.haskell.org/package/base-4.2.0.1/docs/Data-Either.html Haskell Data.Either
|
78
|
+
# @see http://ruby-concurrency.github.io/concurrent-ruby/Concurrent/Obligation.html Concurrent Ruby
|
79
|
+
#
|
80
|
+
# @!macro thread_safe_immutable_object
|
81
|
+
class Either
|
82
|
+
include AbstractStruct
|
83
|
+
|
84
|
+
self.datatype = :either
|
85
|
+
self.fields = [:left, :right].freeze
|
86
|
+
|
87
|
+
# @!visibility private
|
88
|
+
NO_VALUE = Object.new.freeze
|
89
|
+
|
90
|
+
private_class_method :new
|
91
|
+
|
92
|
+
class << self
|
93
|
+
|
94
|
+
# Construct a left value of either.
|
95
|
+
#
|
96
|
+
# @param [Object] value The value underlying the either.
|
97
|
+
# @return [Either] A new either with the given left value.
|
98
|
+
def left(value)
|
99
|
+
new(value, true).freeze
|
100
|
+
end
|
101
|
+
alias_method :reason, :left
|
102
|
+
|
103
|
+
# Construct a right value of either.
|
104
|
+
#
|
105
|
+
# @param [Object] value The value underlying the either.
|
106
|
+
# @return [Either] A new either with the given right value.
|
107
|
+
def right(value)
|
108
|
+
new(value, false).freeze
|
109
|
+
end
|
110
|
+
alias_method :value, :right
|
111
|
+
|
112
|
+
# Create an `Either` with the left value set to an `Exception` object
|
113
|
+
# complete with message and backtrace. This is a convenience method for
|
114
|
+
# supporting the reason/value convention with the reason always being
|
115
|
+
# an `Exception` object. When no exception class is given `StandardError`
|
116
|
+
# will be used. When no message is given the default message for the
|
117
|
+
# given error class will be used.
|
118
|
+
#
|
119
|
+
# @example
|
120
|
+
#
|
121
|
+
# either = Functional::Either.error("You're a bad monkey, Mojo Jojo")
|
122
|
+
# either.fulfilled? #=> false
|
123
|
+
# either.rejected? #=> true
|
124
|
+
# either.value #=> nil
|
125
|
+
# either.reason #=> #<StandardError: You're a bad monkey, Mojo Jojo>
|
126
|
+
#
|
127
|
+
# @param [String] message The message for the new error object.
|
128
|
+
# @param [Exception] clazz The class for the new error object.
|
129
|
+
# @return [Either] A new either with an error object as the left value.
|
130
|
+
def error(message = nil, clazz = StandardError)
|
131
|
+
ex = clazz.new(message)
|
132
|
+
ex.set_backtrace(caller)
|
133
|
+
left(ex)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
# Projects this either as a left.
|
138
|
+
#
|
139
|
+
# @return [Object] The left value or `nil` when `right`.
|
140
|
+
def left
|
141
|
+
left? ? to_h[:left] : nil
|
142
|
+
end
|
143
|
+
alias_method :reason, :left
|
144
|
+
|
145
|
+
# Projects this either as a right.
|
146
|
+
#
|
147
|
+
# @return [Object] The right value or `nil` when `left`.
|
148
|
+
def right
|
149
|
+
right? ? to_h[:right] : nil
|
150
|
+
end
|
151
|
+
alias_method :value, :right
|
152
|
+
|
153
|
+
# Returns true if this either is a left, false otherwise.
|
154
|
+
#
|
155
|
+
# @return [Boolean] `true` if this either is a left, `false` otherwise.
|
156
|
+
def left?
|
157
|
+
@is_left
|
158
|
+
end
|
159
|
+
alias_method :reason?, :left?
|
160
|
+
alias_method :rejected?, :left?
|
161
|
+
|
162
|
+
# Returns true if this either is a right, false otherwise.
|
163
|
+
#
|
164
|
+
# @return [Boolean] `true` if this either is a right, `false` otherwise.
|
165
|
+
def right?
|
166
|
+
! left?
|
167
|
+
end
|
168
|
+
alias_method :value?, :right?
|
169
|
+
alias_method :fulfilled?, :right?
|
170
|
+
|
171
|
+
# If this is a left, then return the left value in right, or vice versa.
|
172
|
+
#
|
173
|
+
# @return [Either] The value of this either swapped to the opposing side.
|
174
|
+
def swap
|
175
|
+
if left?
|
176
|
+
self.class.send(:new, left, false)
|
177
|
+
else
|
178
|
+
self.class.send(:new, right, true)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
# The catamorphism for either. Folds over this either breaking into left or right.
|
183
|
+
#
|
184
|
+
# @param [Proc] lproc The function to call if this is left.
|
185
|
+
# @param [Proc] rproc The function to call if this is right.
|
186
|
+
# @return [Object] The reduced value.
|
187
|
+
def either(lproc, rproc)
|
188
|
+
left? ? lproc.call(left) : rproc.call(right)
|
189
|
+
end
|
190
|
+
|
191
|
+
# If the condition satisfies, return the given A in left, otherwise, return the given B in right.
|
192
|
+
#
|
193
|
+
# @param [Object] lvalue The left value to use if the condition satisfies.
|
194
|
+
# @param [Object] rvalue The right value to use if the condition does not satisfy.
|
195
|
+
# @param [Boolean] condition The condition to test (when no block given).
|
196
|
+
# @yield The condition to test (when no condition given).
|
197
|
+
#
|
198
|
+
# @return [Either] A constructed either based on the given condition.
|
199
|
+
#
|
200
|
+
# @raise [ArgumentError] When both a condition and a block are given.
|
201
|
+
def self.iff(lvalue, rvalue, condition = NO_VALUE)
|
202
|
+
raise ArgumentError.new('requires either a condition or a block, not both') if condition != NO_VALUE && block_given?
|
203
|
+
condition = block_given? ? yield : !! condition
|
204
|
+
condition ? left(lvalue) : right(rvalue)
|
205
|
+
end
|
206
|
+
|
207
|
+
private
|
208
|
+
|
209
|
+
# Create a new Either wil the given value and disposition.
|
210
|
+
#
|
211
|
+
# @param [Object] value the value of this either
|
212
|
+
# @param [Boolean] is_left is this a left either or right?
|
213
|
+
#
|
214
|
+
# @!visibility private
|
215
|
+
def initialize(value, is_left)
|
216
|
+
@is_left = is_left
|
217
|
+
hsh = is_left ? {left: value, right: nil} : {left: nil, right: value}
|
218
|
+
set_data_hash(hsh)
|
219
|
+
set_values_array(hsh.values)
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|