fear 1.0.0 → 2.0.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/.github/dependabot.yml +27 -0
- data/.github/workflows/rubocop.yml +39 -0
- data/.github/workflows/spec.yml +42 -0
- data/.rubocop.yml +4 -60
- data/.simplecov +17 -0
- data/CHANGELOG.md +29 -1
- data/Gemfile +5 -5
- data/Gemfile.lock +86 -50
- data/README.md +240 -209
- data/Rakefile +72 -65
- data/examples/pattern_extracting.rb +10 -8
- data/examples/pattern_matching_binary_tree_set.rb +7 -2
- data/examples/pattern_matching_number_in_words.rb +48 -42
- data/fear.gemspec +33 -34
- data/lib/dry/types/fear/option.rb +125 -0
- data/lib/dry/types/fear.rb +8 -0
- data/lib/fear/await.rb +33 -0
- data/lib/fear/awaitable.rb +28 -0
- data/lib/fear/either.rb +15 -4
- data/lib/fear/either_api.rb +4 -0
- data/lib/fear/either_pattern_match.rb +9 -5
- data/lib/fear/empty_partial_function.rb +3 -1
- data/lib/fear/failure.rb +7 -7
- data/lib/fear/failure_pattern_match.rb +4 -0
- data/lib/fear/for.rb +4 -2
- data/lib/fear/for_api.rb +5 -1
- data/lib/fear/future.rb +157 -82
- data/lib/fear/future_api.rb +17 -4
- data/lib/fear/left.rb +3 -9
- data/lib/fear/left_pattern_match.rb +2 -0
- data/lib/fear/none.rb +28 -10
- data/lib/fear/none_pattern_match.rb +2 -0
- data/lib/fear/option.rb +30 -2
- data/lib/fear/option_api.rb +4 -0
- data/lib/fear/option_pattern_match.rb +8 -3
- data/lib/fear/partial_function/and_then.rb +4 -2
- data/lib/fear/partial_function/any.rb +2 -0
- data/lib/fear/partial_function/combined.rb +3 -1
- data/lib/fear/partial_function/empty.rb +6 -0
- data/lib/fear/partial_function/guard/and.rb +2 -0
- data/lib/fear/partial_function/guard/and3.rb +2 -0
- data/lib/fear/partial_function/guard/or.rb +2 -0
- data/lib/fear/partial_function/guard.rb +8 -6
- data/lib/fear/partial_function/lifted.rb +2 -0
- data/lib/fear/partial_function/or_else.rb +5 -1
- data/lib/fear/partial_function.rb +18 -9
- data/lib/fear/partial_function_class.rb +3 -1
- data/lib/fear/pattern_match.rb +3 -11
- data/lib/fear/pattern_matching_api.rb +6 -28
- data/lib/fear/promise.rb +7 -5
- data/lib/fear/right.rb +3 -9
- data/lib/fear/right_biased.rb +5 -3
- data/lib/fear/right_pattern_match.rb +4 -0
- data/lib/fear/some.rb +35 -8
- data/lib/fear/some_pattern_match.rb +2 -0
- data/lib/fear/struct.rb +237 -0
- data/lib/fear/success.rb +7 -8
- data/lib/fear/success_pattern_match.rb +4 -0
- data/lib/fear/try.rb +8 -2
- data/lib/fear/try_api.rb +4 -0
- data/lib/fear/try_pattern_match.rb +9 -5
- data/lib/fear/unit.rb +6 -2
- data/lib/fear/utils.rb +14 -2
- data/lib/fear/version.rb +4 -1
- data/lib/fear.rb +26 -44
- data/spec/dry/types/fear/option/constrained_spec.rb +22 -0
- data/spec/dry/types/fear/option/core_spec.rb +77 -0
- data/spec/dry/types/fear/option/default_spec.rb +21 -0
- data/spec/dry/types/fear/option/hash_spec.rb +58 -0
- data/spec/dry/types/fear/option/option_spec.rb +97 -0
- data/spec/fear/awaitable_spec.rb +19 -0
- data/spec/fear/done_spec.rb +7 -5
- data/spec/fear/either/mixin_spec.rb +4 -2
- data/spec/fear/either_pattern_match_spec.rb +10 -8
- data/spec/fear/either_pattern_matching_spec.rb +28 -0
- data/spec/fear/either_spec.rb +26 -0
- data/spec/fear/failure_spec.rb +57 -70
- data/spec/fear/for/mixin_spec.rb +15 -0
- data/spec/fear/for_spec.rb +19 -17
- data/spec/fear/future_spec.rb +477 -237
- data/spec/fear/guard_spec.rb +136 -24
- data/spec/fear/left_spec.rb +57 -70
- data/spec/fear/none_spec.rb +39 -43
- data/spec/fear/option/mixin_spec.rb +9 -7
- data/spec/fear/option_pattern_match_spec.rb +10 -8
- data/spec/fear/option_pattern_matching_spec.rb +34 -0
- data/spec/fear/option_spec.rb +142 -0
- data/spec/fear/partial_function/any_spec.rb +25 -0
- data/spec/fear/partial_function/empty_spec.rb +12 -10
- data/spec/fear/partial_function_and_then_spec.rb +39 -37
- data/spec/fear/partial_function_composition_spec.rb +46 -44
- data/spec/fear/partial_function_or_else_spec.rb +92 -90
- data/spec/fear/partial_function_spec.rb +91 -61
- data/spec/fear/pattern_match_spec.rb +19 -51
- data/spec/fear/pattern_matching_api_spec.rb +31 -0
- data/spec/fear/promise_spec.rb +23 -23
- data/spec/fear/right_biased/left.rb +28 -26
- data/spec/fear/right_biased/right.rb +51 -49
- data/spec/fear/right_spec.rb +48 -68
- data/spec/fear/some_spec.rb +30 -40
- data/spec/fear/success_spec.rb +40 -60
- data/spec/fear/try/mixin_spec.rb +19 -3
- data/spec/fear/try_api_spec.rb +23 -0
- data/spec/fear/try_pattern_match_spec.rb +10 -8
- data/spec/fear/try_pattern_matching_spec.rb +34 -0
- data/spec/fear/utils_spec.rb +16 -14
- data/spec/spec_helper.rb +13 -7
- data/spec/struct_pattern_matching_spec.rb +36 -0
- data/spec/struct_spec.rb +194 -0
- data/spec/support/dry_types.rb +6 -0
- metadata +128 -87
- data/.travis.yml +0 -13
- data/lib/fear/extractor/anonymous_array_splat_matcher.rb +0 -8
- data/lib/fear/extractor/any_matcher.rb +0 -15
- data/lib/fear/extractor/array_head_matcher.rb +0 -34
- data/lib/fear/extractor/array_matcher.rb +0 -38
- data/lib/fear/extractor/array_splat_matcher.rb +0 -14
- data/lib/fear/extractor/empty_list_matcher.rb +0 -18
- data/lib/fear/extractor/extractor_matcher.rb +0 -42
- data/lib/fear/extractor/grammar.rb +0 -201
- data/lib/fear/extractor/grammar.treetop +0 -129
- data/lib/fear/extractor/identifier_matcher.rb +0 -16
- data/lib/fear/extractor/matcher/and.rb +0 -36
- data/lib/fear/extractor/matcher.rb +0 -54
- data/lib/fear/extractor/named_array_splat_matcher.rb +0 -15
- data/lib/fear/extractor/pattern.rb +0 -55
- data/lib/fear/extractor/typed_identifier_matcher.rb +0 -24
- data/lib/fear/extractor/value_matcher.rb +0 -17
- data/lib/fear/extractor.rb +0 -108
- data/lib/fear/extractor_api.rb +0 -33
- data/spec/fear/extractor/array_matcher_spec.rb +0 -228
- data/spec/fear/extractor/extractor_matcher_spec.rb +0 -151
- data/spec/fear/extractor/grammar_array_spec.rb +0 -23
- data/spec/fear/extractor/identified_matcher_spec.rb +0 -47
- data/spec/fear/extractor/identifier_matcher_spec.rb +0 -66
- data/spec/fear/extractor/pattern_spec.rb +0 -32
- data/spec/fear/extractor/typed_identifier_matcher_spec.rb +0 -62
- data/spec/fear/extractor/value_matcher_number_spec.rb +0 -77
- data/spec/fear/extractor/value_matcher_string_spec.rb +0 -86
- data/spec/fear/extractor/value_matcher_symbol_spec.rb +0 -69
- data/spec/fear/extractor_api_spec.rb +0 -113
- data/spec/fear/extractor_spec.rb +0 -59
@@ -1,13 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "fear/partial_function/guard/or"
|
4
|
+
require "fear/partial_function/guard/and"
|
5
|
+
require "fear/partial_function/guard/and3"
|
6
|
+
|
1
7
|
module Fear
|
2
8
|
module PartialFunction
|
3
9
|
# Guard represents PartialFunction guardian
|
4
10
|
#
|
5
11
|
# @api private
|
6
12
|
class Guard
|
7
|
-
autoload :And, 'fear/partial_function/guard/and'
|
8
|
-
autoload :And3, 'fear/partial_function/guard/and3'
|
9
|
-
autoload :Or, 'fear/partial_function/guard/or'
|
10
|
-
|
11
13
|
class << self
|
12
14
|
# Optimized version for combination of two guardians
|
13
15
|
# Two guarding is a very common situation. For example checking for Some, and checking
|
@@ -35,7 +37,7 @@ module Fear
|
|
35
37
|
when 0 then Any
|
36
38
|
else
|
37
39
|
head, *tail = conditions
|
38
|
-
tail.
|
40
|
+
tail.reduce(new(head)) { |acc, condition| acc.and(new(condition)) }
|
39
41
|
end
|
40
42
|
end
|
41
43
|
|
@@ -45,7 +47,7 @@ module Fear
|
|
45
47
|
return Any if conditions.empty?
|
46
48
|
|
47
49
|
head, *tail = conditions
|
48
|
-
tail.
|
50
|
+
tail.reduce(new(head)) { |acc, condition| acc.or(new(condition)) }
|
49
51
|
end
|
50
52
|
end
|
51
53
|
|
@@ -1,3 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "fear/utils"
|
4
|
+
|
1
5
|
module Fear
|
2
6
|
module PartialFunction
|
3
7
|
# Composite function produced by +PartialFunction#or_else+ method
|
@@ -36,7 +40,7 @@ module Fear
|
|
36
40
|
|
37
41
|
# @see Fear::PartialFunction#and_then
|
38
42
|
def and_then(other = Utils::UNDEFINED, &block)
|
39
|
-
Utils.with_block_or_argument(
|
43
|
+
Utils.with_block_or_argument("Fear::PartialFunction::OrElse#and_then", other, block) do |fun|
|
40
44
|
OrElse.new(f1.and_then(&fun), f2.and_then(&fun))
|
41
45
|
end
|
42
46
|
end
|
@@ -1,3 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "fear/partial_function/and_then"
|
4
|
+
require "fear/partial_function/any"
|
5
|
+
require "fear/partial_function/combined"
|
6
|
+
require "fear/partial_function/empty"
|
7
|
+
require "fear/partial_function/guard"
|
8
|
+
require "fear/partial_function/lifted"
|
9
|
+
require "fear/partial_function/or_else"
|
10
|
+
require "fear/partial_function_class"
|
11
|
+
|
1
12
|
module Fear
|
2
13
|
# A partial function is a unary function defined on subset of all possible inputs.
|
3
14
|
# The method +defined_at?+ allows to test dynamically if an arg is in
|
@@ -43,14 +54,6 @@ module Fear
|
|
43
54
|
# @return [#call]
|
44
55
|
# @abstract
|
45
56
|
module PartialFunction
|
46
|
-
autoload :AndThen, 'fear/partial_function/and_then'
|
47
|
-
autoload :Any, 'fear/partial_function/any'
|
48
|
-
autoload :Combined, 'fear/partial_function/combined'
|
49
|
-
autoload :EMPTY, 'fear/partial_function/empty'
|
50
|
-
autoload :Guard, 'fear/partial_function/guard'
|
51
|
-
autoload :Lifted, 'fear/partial_function/lifted'
|
52
|
-
autoload :OrElse, 'fear/partial_function/or_else'
|
53
|
-
|
54
57
|
# Checks if a value is contained in the function's domain.
|
55
58
|
#
|
56
59
|
# @param arg [any]
|
@@ -97,6 +100,12 @@ module Fear
|
|
97
100
|
# @param other [PartialFunction]
|
98
101
|
# @return [PartialFunction] a partial function which has as domain the union of the domains
|
99
102
|
# of this partial function and +other+.
|
103
|
+
# @example
|
104
|
+
# handle_even = Fear.case(:even?.to_proc) { |x| "#{x} is even" }
|
105
|
+
# handle_odd = Fear.case(:odd?.to_proc) { |x| "#{x} is odd" }
|
106
|
+
# handle_even_or_odd = handle_even.or_else(odd)
|
107
|
+
# handle_even_or_odd.(42) #=> 42 is even
|
108
|
+
# handle_even_or_odd.(42) #=> 21 is odd
|
100
109
|
def or_else(other)
|
101
110
|
OrElse.new(self, other)
|
102
111
|
end
|
@@ -125,7 +134,7 @@ module Fear
|
|
125
134
|
# @return [Fear::PartialFunction]
|
126
135
|
#
|
127
136
|
def and_then(other = Utils::UNDEFINED, &block)
|
128
|
-
Utils.with_block_or_argument(
|
137
|
+
Utils.with_block_or_argument("Fear::PartialFunction#and_then", other, block) do |fun|
|
129
138
|
if fun.is_a?(Fear::PartialFunction)
|
130
139
|
Combined.new(self, fun)
|
131
140
|
else
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Fear
|
2
4
|
# @api private
|
3
5
|
class PartialFunctionClass
|
@@ -25,7 +27,7 @@ module Fear
|
|
25
27
|
# @yield [arg] if function not defined
|
26
28
|
def call_or_else(arg)
|
27
29
|
if defined_at?(arg)
|
28
|
-
function.
|
30
|
+
function.(arg)
|
29
31
|
else
|
30
32
|
yield arg
|
31
33
|
end
|
data/lib/fear/pattern_match.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Fear
|
2
4
|
# Pattern match builder. Provides DSL for building pattern matcher
|
3
5
|
# Pattern match is just a combination of partial functions
|
@@ -59,7 +61,7 @@ module Fear
|
|
59
61
|
|
60
62
|
Module.new do
|
61
63
|
define_method(as) do |&matchers|
|
62
|
-
matcher_class.new(&matchers).
|
64
|
+
matcher_class.new(&matchers).(self)
|
63
65
|
end
|
64
66
|
end
|
65
67
|
end
|
@@ -93,16 +95,6 @@ module Fear
|
|
93
95
|
or_else(Fear.case(*guards, &effect))
|
94
96
|
end
|
95
97
|
|
96
|
-
# @param pattern [String]
|
97
|
-
# @param guards [<#===>]
|
98
|
-
# @param effect [Proc]
|
99
|
-
# @return [Fear::PartialFunction]
|
100
|
-
# @see #case for details
|
101
|
-
# @see Fear.xcase for details
|
102
|
-
def xcase(pattern, *guards, &effect)
|
103
|
-
or_else(Fear.xcase(pattern, *guards, &effect))
|
104
|
-
end
|
105
|
-
|
106
98
|
# @see Fear::PartialFunction#or_else
|
107
99
|
def or_else(other)
|
108
100
|
self.result = result.or_else(other)
|
@@ -1,3 +1,8 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "fear/partial_function"
|
4
|
+
require "fear/pattern_match"
|
5
|
+
|
1
6
|
module Fear
|
2
7
|
# @api private
|
3
8
|
module PatternMatchingApi
|
@@ -42,12 +47,6 @@ module Fear
|
|
42
47
|
# m.case(Integer, :even?.to_proc) { |x| ... }
|
43
48
|
# m.case(Integer, :odd?.to_proc) { |x| ... }
|
44
49
|
#
|
45
|
-
# If you want to perform pattern destruction, use +#xcase+ method
|
46
|
-
#
|
47
|
-
# m.xcase('Date(year, 12, 31)') { |year:| "Last day of the year #{year}" }
|
48
|
-
#
|
49
|
-
# The pattern above ensures that it's 31 of December and extracts year to block named parameter
|
50
|
-
#
|
51
50
|
# Since matcher returns +Fear::PartialFunction+, you can combine matchers using
|
52
51
|
# partial function API:
|
53
52
|
#
|
@@ -83,7 +82,7 @@ module Fear
|
|
83
82
|
# @yieldparam matcher [Fear::PartialFunction]
|
84
83
|
# @return [any]
|
85
84
|
def match(value, &block)
|
86
|
-
matcher(&block).
|
85
|
+
matcher(&block).(value)
|
87
86
|
end
|
88
87
|
|
89
88
|
# Creates partial function defined on domain described with guards
|
@@ -109,26 +108,5 @@ module Fear
|
|
109
108
|
def case(*guards, &function)
|
110
109
|
PartialFunction.and(*guards, &function)
|
111
110
|
end
|
112
|
-
|
113
|
-
# Creates partial function defined on domain described with guard
|
114
|
-
# and perform pattern extraction.
|
115
|
-
#
|
116
|
-
# @param pattern [String] pattern to match against
|
117
|
-
# @param guards [<#===>] other guards against extracted pattern
|
118
|
-
# @yieldparam hash [{Symbol => any}]
|
119
|
-
# @return [Fear::PartialFunction]
|
120
|
-
#
|
121
|
-
# @example
|
122
|
-
# pf = Fear.xcase('['ok', Some(body)]') { |body:| ... }
|
123
|
-
# pf.defined_at?(['ok', Fear.some(body)]) #=> true
|
124
|
-
# pf.defined_at?(['err', Fear.none]) #=> false
|
125
|
-
#
|
126
|
-
# @example pattern and guards. It matches against non-empty body
|
127
|
-
#
|
128
|
-
# pf = Fear.xcase('['ok', Some(body)]', ->(body:) { !body.empty? }) { }
|
129
|
-
#
|
130
|
-
def xcase(pattern, *guards, &function)
|
131
|
-
Fear[pattern].and_then(self.case(*guards, &function))
|
132
|
-
end
|
133
111
|
end
|
134
112
|
end
|
data/lib/fear/promise.rb
CHANGED
@@ -1,8 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Fear
|
2
4
|
# @api private
|
3
5
|
class Promise < Concurrent::IVar
|
4
|
-
# @param options [Hash] options passed to underlying +Concurrent::
|
5
|
-
def initialize(options
|
6
|
+
# @param options [Hash] options passed to underlying +Concurrent::Promise+
|
7
|
+
def initialize(*_, **options)
|
6
8
|
super()
|
7
9
|
@options = options
|
8
10
|
@promise = Concurrent::Promise.new(options) do
|
@@ -19,7 +21,7 @@ module Fear
|
|
19
21
|
|
20
22
|
# @return [Fear::Future]
|
21
23
|
def to_future
|
22
|
-
Future.new(promise, options)
|
24
|
+
Future.new(promise, **options)
|
23
25
|
end
|
24
26
|
|
25
27
|
# Complete this promise with successful result
|
@@ -30,7 +32,7 @@ module Fear
|
|
30
32
|
complete(Fear.success(value))
|
31
33
|
end
|
32
34
|
|
33
|
-
# Complete this promise with
|
35
|
+
# Complete this promise with value
|
34
36
|
# @param value [any]
|
35
37
|
# @return [self]
|
36
38
|
# @raise [IllegalStateException]
|
@@ -64,7 +66,7 @@ module Fear
|
|
64
66
|
if complete(result)
|
65
67
|
self
|
66
68
|
else
|
67
|
-
raise IllegalStateException,
|
69
|
+
raise IllegalStateException, "Promise already completed."
|
68
70
|
end
|
69
71
|
end
|
70
72
|
|
data/lib/fear/right.rb
CHANGED
@@ -1,17 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Fear
|
2
4
|
class Right
|
3
5
|
include Either
|
4
6
|
include RightBiased::Right
|
5
7
|
include RightPatternMatch.mixin
|
6
8
|
|
7
|
-
EXTRACTOR = proc do |either|
|
8
|
-
if Fear::Right === either
|
9
|
-
Fear.some([either.right_value])
|
10
|
-
else
|
11
|
-
Fear.none
|
12
|
-
end
|
13
|
-
end
|
14
|
-
|
15
9
|
# @api private
|
16
10
|
def right_value
|
17
11
|
value
|
@@ -65,7 +59,7 @@ module Fear
|
|
65
59
|
# @param reduce_right [Proc]
|
66
60
|
# @return [any]
|
67
61
|
def reduce(_reduce_left, reduce_right)
|
68
|
-
reduce_right.
|
62
|
+
reduce_right.(value)
|
69
63
|
end
|
70
64
|
|
71
65
|
# @return [Either]
|
data/lib/fear/right_biased.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Fear
|
2
4
|
# @private
|
3
5
|
module RightBiased
|
@@ -7,14 +9,14 @@ module Fear
|
|
7
9
|
# Returns the value from this `RightBiased::Right` or the given argument if
|
8
10
|
# this is a `RightBiased::Left`.
|
9
11
|
def get_or_else(*args, &block)
|
10
|
-
Utils.assert_arg_or_block!(
|
12
|
+
Utils.assert_arg_or_block!("get_or_else", *args, &block)
|
11
13
|
super
|
12
14
|
end
|
13
15
|
|
14
16
|
# Returns this `RightBiased::Right` or the given alternative if
|
15
17
|
# this is a `RightBiased::Left`.
|
16
18
|
def or_else(*args, &block)
|
17
|
-
Utils.assert_arg_or_block!(
|
19
|
+
Utils.assert_arg_or_block!("or_else", *args, &block)
|
18
20
|
super.tap do |result|
|
19
21
|
Utils.assert_type!(result, left_class, right_class)
|
20
22
|
end
|
@@ -109,7 +111,7 @@ module Fear
|
|
109
111
|
|
110
112
|
module Left
|
111
113
|
prepend Interface
|
112
|
-
|
114
|
+
|
113
115
|
# @!method get_or_else(default)
|
114
116
|
# @param default [any]
|
115
117
|
# @return [any] default value
|
data/lib/fear/some.rb
CHANGED
@@ -1,17 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Fear
|
2
4
|
class Some
|
3
5
|
include Option
|
4
6
|
include RightBiased::Right
|
5
7
|
include SomePatternMatch.mixin
|
6
8
|
|
7
|
-
EXTRACTOR = proc do |option|
|
8
|
-
if Fear::Some === option
|
9
|
-
Fear.some([option.get])
|
10
|
-
else
|
11
|
-
Fear.none
|
12
|
-
end
|
13
|
-
end
|
14
|
-
|
15
9
|
attr_reader :value
|
16
10
|
protected :value
|
17
11
|
|
@@ -34,6 +28,13 @@ module Fear
|
|
34
28
|
false
|
35
29
|
end
|
36
30
|
|
31
|
+
alias :blank? :empty?
|
32
|
+
|
33
|
+
# @return [true]
|
34
|
+
def present?
|
35
|
+
true
|
36
|
+
end
|
37
|
+
|
37
38
|
# @return [Option]
|
38
39
|
def select
|
39
40
|
if yield(value)
|
@@ -65,5 +66,31 @@ module Fear
|
|
65
66
|
|
66
67
|
# @return [String]
|
67
68
|
alias to_s inspect
|
69
|
+
|
70
|
+
# @param other [Fear::Option]
|
71
|
+
# @return [Fear::Option]
|
72
|
+
def zip(other)
|
73
|
+
if other.is_a?(Option)
|
74
|
+
other.map do |x|
|
75
|
+
if block_given?
|
76
|
+
yield(value, x)
|
77
|
+
else
|
78
|
+
[value, x]
|
79
|
+
end
|
80
|
+
end
|
81
|
+
else
|
82
|
+
raise TypeError, "can't zip with #{other.class}"
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# @return [RightBiased::Left, RightBiased::Right]
|
87
|
+
def filter_map(&filter)
|
88
|
+
map(&filter).select(&:itself)
|
89
|
+
end
|
90
|
+
|
91
|
+
# @return [Array<any>]
|
92
|
+
def deconstruct
|
93
|
+
[get]
|
94
|
+
end
|
68
95
|
end
|
69
96
|
end
|
data/lib/fear/struct.rb
ADDED
@@ -0,0 +1,237 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "fear/pattern_match"
|
4
|
+
|
5
|
+
module Fear
|
6
|
+
# Structs are like regular classes and good for modeling immutable data.
|
7
|
+
#
|
8
|
+
# A minimal struct requires just a list of attributes:
|
9
|
+
#
|
10
|
+
# User = Fear::Struct.with_attributes(:id, :email, :admin)
|
11
|
+
# john = User.new(id: 2, email: 'john@example.com', admin: false)
|
12
|
+
#
|
13
|
+
# john.email #=> 'john@example.com'
|
14
|
+
#
|
15
|
+
# Instead of `.with_attributes` factory method you can use classic inheritance:
|
16
|
+
#
|
17
|
+
# class User < Fear::Struct
|
18
|
+
# attribute :id
|
19
|
+
# attribute :email
|
20
|
+
# attribute :admin
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# Since structs are immutable, you are not allowed to reassign their attributes
|
24
|
+
#
|
25
|
+
# john.email = ''john.doe@example.com'' #=> raises NoMethodError
|
26
|
+
#
|
27
|
+
# Two structs of the same type with the same attributes are equal
|
28
|
+
#
|
29
|
+
# john1 = User.new(id: 2, email: 'john@example.com', admin: false)
|
30
|
+
# john2 = User.new(id: 2, admin: false, email: 'john@example.com')
|
31
|
+
# john1 == john2 #=> true
|
32
|
+
#
|
33
|
+
# You can create a shallow copy of a +Struct+ by using copy method optionally changing its attributes.
|
34
|
+
#
|
35
|
+
# john = User.new(id: 2, email: 'john@example.com', admin: false)
|
36
|
+
# admin_john = john.copy(admin: true)
|
37
|
+
#
|
38
|
+
# john.admin #=> false
|
39
|
+
# admin_john.admin #=> true
|
40
|
+
#
|
41
|
+
class Struct
|
42
|
+
include PatternMatch.mixin
|
43
|
+
|
44
|
+
@attributes = [].freeze
|
45
|
+
|
46
|
+
class << self
|
47
|
+
# @param base [Fear::Struct]
|
48
|
+
# @api private
|
49
|
+
def inherited(base)
|
50
|
+
base.instance_variable_set(:@attributes, attributes)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Defines attribute
|
54
|
+
#
|
55
|
+
# @param name [Symbol]
|
56
|
+
# @return [Symbol] attribute name
|
57
|
+
#
|
58
|
+
# @example
|
59
|
+
# class User < Fear::Struct
|
60
|
+
# attribute :id
|
61
|
+
# attribute :email
|
62
|
+
# end
|
63
|
+
#
|
64
|
+
def attribute(name)
|
65
|
+
name.to_sym.tap do |symbolized_name|
|
66
|
+
@attributes << symbolized_name
|
67
|
+
attr_reader symbolized_name
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Members of this struct
|
72
|
+
#
|
73
|
+
# @return [<Symbol>]
|
74
|
+
def attributes
|
75
|
+
@attributes.dup
|
76
|
+
end
|
77
|
+
|
78
|
+
# Creates new struct with given attributes
|
79
|
+
# @param members [<Symbol>]
|
80
|
+
# @return [Fear::Struct]
|
81
|
+
#
|
82
|
+
# @example
|
83
|
+
# User = Fear::Struct.with_attributes(:id, :email, :admin) do
|
84
|
+
# def admin?
|
85
|
+
# @admin
|
86
|
+
# end
|
87
|
+
# end
|
88
|
+
#
|
89
|
+
def with_attributes(*members, &block)
|
90
|
+
members = members
|
91
|
+
block = block
|
92
|
+
|
93
|
+
Class.new(self) do
|
94
|
+
members.each { |member| attribute(member) }
|
95
|
+
class_eval(&block) if block
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# @param attributes [{Symbol => any}]
|
101
|
+
def initialize(**attributes)
|
102
|
+
_check_missing_attributes!(attributes)
|
103
|
+
_check_unknown_attributes!(attributes)
|
104
|
+
|
105
|
+
@values = members.each_with_object([]) do |name, values|
|
106
|
+
attributes.fetch(name).tap do |value|
|
107
|
+
_set_attribute(name, value)
|
108
|
+
values << value
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# Creates a shallow copy of this struct optionally changing the attributes arguments.
|
114
|
+
# @param attributes [{Symbol => any}]
|
115
|
+
#
|
116
|
+
# @example
|
117
|
+
# User = Fear::Struct.new(:id, :email, :admin)
|
118
|
+
# john = User.new(id: 2, email: 'john@example.com', admin: false)
|
119
|
+
# john.admin #=> false
|
120
|
+
# admin_john = john.copy(admin: true)
|
121
|
+
# admin_john.admin #=> true
|
122
|
+
#
|
123
|
+
def copy(**attributes)
|
124
|
+
self.class.new(**to_h.merge(attributes))
|
125
|
+
end
|
126
|
+
|
127
|
+
# Returns the struct attributes as an array of symbols
|
128
|
+
# @return [<Symbol>]
|
129
|
+
#
|
130
|
+
# @example
|
131
|
+
# User = Fear::Struct.new(:id, :email, :admin)
|
132
|
+
# john = User.new(email: 'john@example.com', admin: false, id: 2)
|
133
|
+
# john.attributes #=> [:id, :email, :admin]
|
134
|
+
#
|
135
|
+
def members
|
136
|
+
self.class.attributes
|
137
|
+
end
|
138
|
+
|
139
|
+
# Returns the values for this struct as an Array.
|
140
|
+
# @return [Array]
|
141
|
+
#
|
142
|
+
# @example
|
143
|
+
# User = Fear::Struct.new(:id, :email, :admin)
|
144
|
+
# john = User.new(email: 'john@example.com', admin: false, id: 2)
|
145
|
+
# john.to_a #=> [2, 'john@example.com', false]
|
146
|
+
#
|
147
|
+
def to_a
|
148
|
+
@values.dup
|
149
|
+
end
|
150
|
+
|
151
|
+
# @overload to_h()
|
152
|
+
# Returns a Hash containing the names and values for the struct's attributes
|
153
|
+
# @return [{Symbol => any}]
|
154
|
+
#
|
155
|
+
# @overload to_h(&block)
|
156
|
+
# Applies block to pairs of name name and value and use them to construct hash
|
157
|
+
# @yieldparam pair [<Symbol, any>] yields pair of name name and value
|
158
|
+
# @return [{Symbol => any}]
|
159
|
+
#
|
160
|
+
# @example
|
161
|
+
# User = Fear::Struct.new(:id, :email, :admin)
|
162
|
+
# john = User.new(email: 'john@example.com', admin: false, id: 2)
|
163
|
+
# john.to_h #=> {id: 2, email: 'john@example.com', admin: false}
|
164
|
+
# john.to_h do |key, value|
|
165
|
+
# [key.to_s, value]
|
166
|
+
# end #=> {'id' => 2, 'email' => 'john@example.com', 'admin' => false}
|
167
|
+
#
|
168
|
+
def to_h(&block)
|
169
|
+
pairs = members.zip(@values)
|
170
|
+
if block_given?
|
171
|
+
Hash[pairs.map(&block)]
|
172
|
+
else
|
173
|
+
Hash[pairs]
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
# @param other [any]
|
178
|
+
# @return [Boolean]
|
179
|
+
def ==(other)
|
180
|
+
other.is_a?(other.class) && to_h == other.to_h
|
181
|
+
end
|
182
|
+
|
183
|
+
INSPECT_TEMPLATE = "<#Fear::Struct %{class_name} %{attributes}>"
|
184
|
+
private_constant :INSPECT_TEMPLATE
|
185
|
+
|
186
|
+
# @return [String]
|
187
|
+
#
|
188
|
+
# @example
|
189
|
+
# User = Fear::Struct.with_attributes(:id, :email)
|
190
|
+
# user = User.new(id: 2, email: 'john@exmaple.com')
|
191
|
+
# user.inspect #=> "<#Fear::Struct User id=2, email=>'john@exmaple.com'>"
|
192
|
+
#
|
193
|
+
def inspect
|
194
|
+
attributes = to_h.map { |key, value| "#{key}=#{value.inspect}" }.join(", ")
|
195
|
+
|
196
|
+
format(INSPECT_TEMPLATE, class_name: self.class.name, attributes: attributes)
|
197
|
+
end
|
198
|
+
alias to_s inspect
|
199
|
+
|
200
|
+
MISSING_KEYWORDS_ERROR = "missing keywords: %{keywords}"
|
201
|
+
private_constant :MISSING_KEYWORDS_ERROR
|
202
|
+
|
203
|
+
private def _check_missing_attributes!(provided_attributes)
|
204
|
+
missing_attributes = members - provided_attributes.keys
|
205
|
+
|
206
|
+
unless missing_attributes.empty?
|
207
|
+
raise ArgumentError, format(MISSING_KEYWORDS_ERROR, keywords: missing_attributes.join(", "))
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
UNKNOWN_KEYWORDS_ERROR = "unknown keywords: %{keywords}"
|
212
|
+
private_constant :UNKNOWN_KEYWORDS_ERROR
|
213
|
+
|
214
|
+
private def _check_unknown_attributes!(provided_attributes)
|
215
|
+
unknown_attributes = provided_attributes.keys - members
|
216
|
+
|
217
|
+
unless unknown_attributes.empty?
|
218
|
+
raise ArgumentError, format(UNKNOWN_KEYWORDS_ERROR, keywords: unknown_attributes.join(", "))
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
# @return [void]
|
223
|
+
private def _set_attribute(name, value)
|
224
|
+
instance_variable_set(:"@#{name}", value)
|
225
|
+
end
|
226
|
+
|
227
|
+
# @param keys [Hash, nil]
|
228
|
+
# @return [Hash]
|
229
|
+
def deconstruct_keys(keys)
|
230
|
+
if keys
|
231
|
+
to_h.slice(*(self.class.attributes & keys))
|
232
|
+
else
|
233
|
+
to_h
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|