fear 0.10.0 → 0.11.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/.rubocop.yml +30 -4
- data/.travis.yml +2 -3
- data/Appraisals +5 -9
- data/CHANGELOG.md +9 -0
- data/Gemfile +2 -0
- data/LICENSE.txt +1 -1
- data/README.md +255 -85
- data/Rakefile +393 -0
- data/fear.gemspec +13 -6
- data/gemfiles/dry_equalizer_0.1.0.gemfile +1 -0
- data/gemfiles/dry_equalizer_0.1.0.gemfile.lock +31 -27
- data/gemfiles/dry_equalizer_0.2.1.gemfile +1 -0
- data/gemfiles/dry_equalizer_0.2.1.gemfile.lock +31 -27
- data/lib/fear/either.rb +49 -14
- data/lib/fear/either_pattern_match.rb +48 -0
- data/lib/fear/empty_partial_function.rb +36 -0
- data/lib/fear/failure.rb +5 -4
- data/lib/fear/failure_pattern_match.rb +8 -0
- data/lib/fear/for.rb +46 -51
- data/lib/fear/left.rb +7 -1
- data/lib/fear/left_pattern_match.rb +9 -0
- data/lib/fear/none.rb +37 -2
- data/lib/fear/none_pattern_match.rb +12 -0
- data/lib/fear/option.rb +65 -31
- data/lib/fear/option_pattern_match.rb +45 -0
- data/lib/fear/partial_function/and_then.rb +48 -0
- data/lib/fear/partial_function/any.rb +26 -0
- data/lib/fear/partial_function/combined.rb +51 -0
- data/lib/fear/partial_function/empty.rb +6 -0
- data/lib/fear/partial_function/guard/and.rb +36 -0
- data/lib/fear/partial_function/guard/and3.rb +39 -0
- data/lib/fear/partial_function/guard/or.rb +36 -0
- data/lib/fear/partial_function/guard.rb +90 -0
- data/lib/fear/partial_function/lifted.rb +20 -0
- data/lib/fear/partial_function/or_else.rb +62 -0
- data/lib/fear/partial_function.rb +171 -0
- data/lib/fear/partial_function_class.rb +26 -0
- data/lib/fear/pattern_match.rb +102 -0
- data/lib/fear/pattern_matching_api.rb +110 -0
- data/lib/fear/right.rb +7 -1
- data/lib/fear/right_biased.rb +2 -12
- data/lib/fear/right_pattern_match.rb +9 -0
- data/lib/fear/some.rb +5 -2
- data/lib/fear/some_pattern_match.rb +11 -0
- data/lib/fear/success.rb +5 -4
- data/lib/fear/success_pattern_match.rb +10 -0
- data/lib/fear/try.rb +56 -16
- data/lib/fear/try_pattern_match.rb +28 -0
- data/lib/fear/utils.rb +24 -14
- data/lib/fear/version.rb +1 -1
- data/lib/fear.rb +21 -4
- data/spec/fear/either_pattern_match_spec.rb +37 -0
- data/spec/fear/failure_spec.rb +41 -3
- data/spec/fear/for_spec.rb +17 -29
- data/spec/fear/guard_spec.rb +101 -0
- data/spec/fear/left_spec.rb +38 -0
- data/spec/fear/none_spec.rb +80 -0
- data/spec/fear/option_pattern_match_spec.rb +35 -0
- data/spec/fear/partial_function/empty_spec.rb +36 -0
- data/spec/fear/partial_function_and_then_spec.rb +145 -0
- data/spec/fear/partial_function_composition_spec.rb +80 -0
- data/spec/fear/partial_function_or_else_spec.rb +274 -0
- data/spec/fear/partial_function_spec.rb +165 -0
- data/spec/fear/pattern_match_spec.rb +59 -0
- data/spec/fear/right_biased/left.rb +1 -6
- data/spec/fear/right_biased/right.rb +0 -5
- data/spec/fear/right_spec.rb +38 -0
- data/spec/fear/some_spec.rb +37 -0
- data/spec/fear/success_spec.rb +41 -4
- data/spec/fear/try_pattern_match_spec.rb +37 -0
- metadata +97 -23
- data/lib/fear/for/evaluation_context.rb +0 -91
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f2a2b3d7163ba41625120e4681b75a845e5d9da9
|
4
|
+
data.tar.gz: 5d85fb64e778587e9f8f85695acb4f19602ae47e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 99ff3b3886920d253e835b6d1346ff7680f26650a01087cbd83629ec3ba3e7a152ef7714937c653639eef1689dcbac242c7a18cfd26db8d41155f5f4cc9bd80b
|
7
|
+
data.tar.gz: 7fcabd7b3cada123a3d2988c4cc4c19bf2af8195e10ff151a886f9a24e1b12dfa73061ca9bc323c056aee9f0be985886b74a02b23a2eeacc10d39e8da2735fb5
|
data/.rubocop.yml
CHANGED
@@ -1,11 +1,11 @@
|
|
1
|
-
inherit_gem:
|
2
|
-
spbtv_code_style: .strict_rubocop.yml
|
3
|
-
|
4
1
|
AllCops:
|
5
2
|
Exclude:
|
6
3
|
- 'gemfiles/**/*'
|
7
4
|
|
8
|
-
|
5
|
+
Naming/MethodName:
|
6
|
+
Enabled: false
|
7
|
+
|
8
|
+
Naming/UncommunicativeMethodParamName:
|
9
9
|
Enabled: false
|
10
10
|
|
11
11
|
Style/Documentation:
|
@@ -14,6 +14,32 @@ Style/Documentation:
|
|
14
14
|
Style/CaseEquality:
|
15
15
|
Enabled: false
|
16
16
|
|
17
|
+
Style/AccessModifierDeclarations:
|
18
|
+
EnforcedStyle: inline
|
19
|
+
|
20
|
+
Style/GuardClause:
|
21
|
+
Enabled: false
|
22
|
+
|
23
|
+
Style/IfUnlessModifier:
|
24
|
+
Enabled: false
|
25
|
+
|
26
|
+
Style/TrailingCommaInArguments:
|
27
|
+
EnforcedStyleForMultiline: comma
|
28
|
+
|
29
|
+
Style/TrailingCommaInArrayLiteral:
|
30
|
+
EnforcedStyleForMultiline: comma
|
31
|
+
|
32
|
+
Style/TrailingCommaInHashLiteral:
|
33
|
+
EnforcedStyleForMultiline: comma
|
34
|
+
|
35
|
+
Style/NumericPredicate:
|
36
|
+
Enabled: false
|
37
|
+
|
17
38
|
Metrics/BlockLength:
|
18
39
|
Exclude:
|
19
40
|
- spec/**/*
|
41
|
+
- Rakefile
|
42
|
+
- fear.gemspec
|
43
|
+
|
44
|
+
Metrics/LineLength:
|
45
|
+
Max: 120
|
data/.travis.yml
CHANGED
@@ -1,4 +1,6 @@
|
|
1
1
|
---
|
2
|
+
cache:
|
3
|
+
bundler: true
|
2
4
|
language: ruby
|
3
5
|
rvm:
|
4
6
|
- 2.4.5
|
@@ -12,6 +14,3 @@ script:
|
|
12
14
|
gemfile:
|
13
15
|
- gemfiles/dry_equalizer_0.2.1.gemfile
|
14
16
|
- gemfiles/dry_equalizer_0.1.0.gemfile
|
15
|
-
addons:
|
16
|
-
code_climate:
|
17
|
-
repo_token: c326cca5984d0e32d2c6b5d9b985756ee9312f63fc6a9480fc9cfa55c454d68a
|
data/Appraisals
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
require 'yaml'
|
2
2
|
|
3
|
-
ruby_versions = %w
|
4
|
-
dry_equalizer_versions = %w
|
3
|
+
ruby_versions = %w[2.4.5 2.5.3 2.6.1]
|
4
|
+
dry_equalizer_versions = %w[0.1.0 0.2.1]
|
5
5
|
|
6
6
|
dry_equalizer_versions.each do |version|
|
7
7
|
appraise "dry-equalizer-#{version}" do
|
@@ -11,6 +11,9 @@ end
|
|
11
11
|
|
12
12
|
Dir.glob('gemfiles/*.gemfile').tap do |gemfiles|
|
13
13
|
travis = ::YAML.dump(
|
14
|
+
'cache' => {
|
15
|
+
'bundler' => true,
|
16
|
+
},
|
14
17
|
'language' => 'ruby',
|
15
18
|
'rvm' => ruby_versions,
|
16
19
|
'before_script' => [
|
@@ -21,13 +24,6 @@ Dir.glob('gemfiles/*.gemfile').tap do |gemfiles|
|
|
21
24
|
'bundle exec rubocop --fail-level C',
|
22
25
|
],
|
23
26
|
'gemfile' => gemfiles,
|
24
|
-
'addons' => {
|
25
|
-
'code_climate' => {
|
26
|
-
'repo_token' => 'c326cca5984d0e32d2c6b5d9b985756ee9312f63fc6a9480fc9cfa55c454d68a'
|
27
|
-
}
|
28
|
-
}
|
29
|
-
|
30
|
-
|
31
27
|
)
|
32
28
|
|
33
29
|
::File.open('.travis.yml', 'w+') do |file|
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,12 @@
|
|
1
|
+
## 0.11.0
|
2
|
+
|
3
|
+
* Implement pattern matching and partial functions. See [README](https://github.com/bolshakov/fear#pattern-matching-api-documentation) (([@bolshakov][]))
|
4
|
+
* `#to_a` method removed ([@bolshakov][])
|
5
|
+
* `For` syntax changed. See [diff](https://github.com/bolshakov/fear/pull/22/files#diff-04c6e90faac2675aa89e2176d2eec7d8) ([@bolshakov][])
|
6
|
+
* `Fear::None` is singleton object now and could not be instantiated ([@bolshakov][])
|
7
|
+
|
8
|
+
You have to change all `Fear::None.new` invocations to `Fear::None`.
|
9
|
+
|
1
10
|
## 0.10.0
|
2
11
|
|
3
12
|
* Test against last supported ruby versions: 2.4.5, 2.5.3, 2.6.1 ([@bolshakov][])
|
data/Gemfile
CHANGED
data/LICENSE.txt
CHANGED
data/README.md
CHANGED
@@ -1,7 +1,5 @@
|
|
1
1
|
# Fear
|
2
2
|
[](https://travis-ci.org/bolshakov/fear)
|
3
|
-
[](https://codeclimate.com/github/bolshakov/functional/maintainability)
|
4
|
-
[](https://codeclimate.com/github/bolshakov/functional/test_coverage)
|
5
3
|
[](https://badge.fury.io/rb/fear)
|
6
4
|
|
7
5
|
This gem provides `Option`, `Either`, and `Try` monads implemented an idiomatic way.
|
@@ -29,7 +27,7 @@ Or install it yourself as:
|
|
29
27
|
* [Try](#try-documentation)
|
30
28
|
* [Either](#either-documentation)
|
31
29
|
* [For composition](#for-composition)
|
32
|
-
* [Pattern Matching](#pattern-matching)
|
30
|
+
* [Pattern Matching](#pattern-matching-api-documentation)
|
33
31
|
|
34
32
|
### Option ([Documentation](http://www.rubydoc.info/github/bolshakov/fear/master/Fear/Option))
|
35
33
|
|
@@ -50,12 +48,9 @@ having to check for the existence of a value.
|
|
50
48
|
A less-idiomatic way to use `Option` values is via pattern matching
|
51
49
|
|
52
50
|
```ruby
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
puts name.strip.upcase
|
57
|
-
when None
|
58
|
-
puts 'No name value'
|
51
|
+
Option(params[:name]).match do |m|
|
52
|
+
m.some { |name| name.strip.upcase }
|
53
|
+
m.none { 'No name value' }
|
59
54
|
end
|
60
55
|
```
|
61
56
|
|
@@ -76,10 +71,10 @@ Returns the value from this `Some` or evaluates the given default argument if th
|
|
76
71
|
|
77
72
|
```ruby
|
78
73
|
Some(42).get_or_else { 24/2 } #=> 42
|
79
|
-
None
|
74
|
+
None.get_or_else { 24/2 } #=> 12
|
80
75
|
|
81
76
|
Some(42).get_or_else(12) #=> 42
|
82
|
-
None
|
77
|
+
None.get_or_else(12) #=> 12
|
83
78
|
```
|
84
79
|
|
85
80
|
#### Option#or_else
|
@@ -88,8 +83,8 @@ returns self `Some` or the given alternative if this is a `None`.
|
|
88
83
|
|
89
84
|
```ruby
|
90
85
|
Some(42).or_else { Some(21) } #=> Some(42)
|
91
|
-
None
|
92
|
-
None
|
86
|
+
None.or_else { Some(21) } #=> Some(21)
|
87
|
+
None.or_else { None } #=> None
|
93
88
|
```
|
94
89
|
|
95
90
|
#### Option#inlude?
|
@@ -99,7 +94,7 @@ Checks if `Option` has an element that is equal (as determined by `==`) to given
|
|
99
94
|
```ruby
|
100
95
|
Some(17).include?(17) #=> true
|
101
96
|
Some(17).include?(7) #=> false
|
102
|
-
None
|
97
|
+
None.include?(17) #=> false
|
103
98
|
```
|
104
99
|
|
105
100
|
#### Option#each
|
@@ -108,7 +103,7 @@ Performs the given block if this is a `Some`.
|
|
108
103
|
|
109
104
|
```ruby
|
110
105
|
Some(17).each { |value| puts value } #=> prints 17
|
111
|
-
None
|
106
|
+
None.each { |value| puts value } #=> does nothing
|
112
107
|
```
|
113
108
|
|
114
109
|
#### Option#map
|
@@ -117,7 +112,7 @@ Maps the given block to the value from this `Some` or returns self if this is a
|
|
117
112
|
|
118
113
|
```ruby
|
119
114
|
Some(42).map { |v| v/2 } #=> Some(21)
|
120
|
-
None
|
115
|
+
None.map { |v| v/2 } #=> None
|
121
116
|
```
|
122
117
|
|
123
118
|
#### Option#flat_map
|
@@ -126,16 +121,7 @@ Returns the given block applied to the value from this `Some` or returns self if
|
|
126
121
|
|
127
122
|
```ruby
|
128
123
|
Some(42).flat_map { |v| Some(v/2) } #=> Some(21)
|
129
|
-
None
|
130
|
-
```
|
131
|
-
|
132
|
-
#### Option#to_a
|
133
|
-
|
134
|
-
Returns an `Array` containing the `Some` value or an empty `Array` if this is a `None`
|
135
|
-
|
136
|
-
```ruby
|
137
|
-
Some(42).to_a #=> [21]
|
138
|
-
None().to_a #=> []
|
124
|
+
None.flat_map { |v| Some(v/2) } #=> None
|
139
125
|
```
|
140
126
|
|
141
127
|
#### Option#any?
|
@@ -145,7 +131,7 @@ Returns `false` if `None` or returns the result of the application of the given
|
|
145
131
|
```ruby
|
146
132
|
Some(12).any?( |v| v > 10) #=> true
|
147
133
|
Some(7).any?( |v| v > 10) #=> false
|
148
|
-
None
|
134
|
+
None.any?( |v| v > 10) #=> false
|
149
135
|
```
|
150
136
|
|
151
137
|
#### Option#select
|
@@ -155,8 +141,8 @@ return `None`.
|
|
155
141
|
|
156
142
|
```ruby
|
157
143
|
Some(42).select { |v| v > 40 } #=> Success(21)
|
158
|
-
Some(42).select { |v| v < 40 } #=> None
|
159
|
-
None
|
144
|
+
Some(42).select { |v| v < 40 } #=> None
|
145
|
+
None.select { |v| v < 40 } #=> None
|
160
146
|
```
|
161
147
|
|
162
148
|
#### Option#reject
|
@@ -166,7 +152,7 @@ Returns `Some` if applying the predicate to this `Option`'s value returns `false
|
|
166
152
|
```ruby
|
167
153
|
Some(42).reject { |v| v > 40 } #=> None
|
168
154
|
Some(42).reject { |v| v < 40 } #=> Some(42)
|
169
|
-
None
|
155
|
+
None.reject { |v| v < 40 } #=> None
|
170
156
|
```
|
171
157
|
|
172
158
|
#### Option#get
|
@@ -179,7 +165,7 @@ Returns `true` if the `Option` is `None`, `false` otherwise.
|
|
179
165
|
|
180
166
|
```ruby
|
181
167
|
Some(42).empty? #=> false
|
182
|
-
None
|
168
|
+
None.empty? #=> true
|
183
169
|
```
|
184
170
|
|
185
171
|
@see https://github.com/scala/scala/blob/2.11.x/src/library/scala/Option.scala
|
@@ -203,11 +189,19 @@ dividend = Try { Integer(params[:dividend]) }
|
|
203
189
|
divisor = Try { Integer(params[:divisor]) }
|
204
190
|
problem = dividend.flat_map { |x| divisor.map { |y| x / y } }
|
205
191
|
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
192
|
+
problem.match |m|
|
193
|
+
m.success do |result|
|
194
|
+
puts "Result of #{dividend.get} / #{divisor.get} is: #{result}"
|
195
|
+
end
|
196
|
+
|
197
|
+
m.failure(ZeroDivisionError) do
|
198
|
+
puts "Division by zero is not allowed"
|
199
|
+
end
|
200
|
+
|
201
|
+
m.failure do |exception|
|
202
|
+
puts "You entered something wrong. Try again"
|
203
|
+
puts "Info from the exception: #{exception.message}"
|
204
|
+
end
|
211
205
|
end
|
212
206
|
```
|
213
207
|
|
@@ -271,22 +265,13 @@ Success(42).flat_map { |v| Success(v/2) } #=> Success(21)
|
|
271
265
|
Failure(ArgumentError.new).flat_map { |v| Success(v/2) } #=> Failure(ArgumentError.new)
|
272
266
|
```
|
273
267
|
|
274
|
-
#### Try#to_a
|
275
|
-
|
276
|
-
Returns an `Array` containing the `Success` value or an empty `Array` if this is a `Failure`.
|
277
|
-
|
278
|
-
```ruby
|
279
|
-
Success(42).to_a #=> [21]
|
280
|
-
Failure(ArgumentError.new).to_a #=> []
|
281
|
-
```
|
282
|
-
|
283
268
|
#### Try#to_option
|
284
269
|
|
285
270
|
Returns an `Some` containing the `Success` value or a `None` if this is a `Failure`.
|
286
271
|
|
287
272
|
```ruby
|
288
273
|
Success(42).to_option #=> Some(21)
|
289
|
-
Failure(ArgumentError.new).to_option #=> None
|
274
|
+
Failure(ArgumentError.new).to_option #=> None
|
290
275
|
```
|
291
276
|
|
292
277
|
#### Try#any?
|
@@ -362,7 +347,7 @@ Success(42).recover_with { |e| Success(e.massage) }
|
|
362
347
|
#=> Success(42)
|
363
348
|
Failure(ArgumentError.new).recover_with { |e| Success(e.massage) }
|
364
349
|
#=> Success('ArgumentError')
|
365
|
-
Failure(ArgumentError.new).recover_with { |e|
|
350
|
+
Failure(ArgumentError.new).recover_with { |e| raise }
|
366
351
|
#=> Failure(RuntimeError)
|
367
352
|
```
|
368
353
|
|
@@ -375,7 +360,7 @@ Success(42).recover { |e| e.massage }
|
|
375
360
|
#=> Success(42)
|
376
361
|
Failure(ArgumentError.new).recover { |e| e.massage }
|
377
362
|
#=> Success('ArgumentError')
|
378
|
-
Failure(ArgumentError.new).recover { |e|
|
363
|
+
Failure(ArgumentError.new).recover { |e| raise }
|
379
364
|
#=> Failure(RuntimeError)
|
380
365
|
```
|
381
366
|
|
@@ -410,12 +395,15 @@ rescue ArgumentError
|
|
410
395
|
Left(in)
|
411
396
|
end
|
412
397
|
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
398
|
+
result.match do |m|
|
399
|
+
m.right do |x|
|
400
|
+
"You passed me the Int: #{x}, which I will increment. #{x} + 1 = #{x+1}"
|
401
|
+
end
|
402
|
+
|
403
|
+
m.left do |x|
|
404
|
+
"You passed me the String: #{x}"
|
405
|
+
end
|
406
|
+
end
|
419
407
|
```
|
420
408
|
|
421
409
|
Either is right-biased, which means that `Right` is assumed to be the default case to
|
@@ -481,22 +469,13 @@ Right(42).flat_map { |v| Right(v/2) } #=> Right(21)
|
|
481
469
|
Left('undefined').flat_map { |v| Right(v/2) } #=> Left('undefined')
|
482
470
|
```
|
483
471
|
|
484
|
-
#### Either#to_a
|
485
|
-
|
486
|
-
Returns an `Array` containing the `Right` value or an empty `Array` if this is a `Left`.
|
487
|
-
|
488
|
-
```ruby
|
489
|
-
Right(42).to_a #=> [21]
|
490
|
-
Left('undefined').to_a #=> []
|
491
|
-
```
|
492
|
-
|
493
472
|
#### Either#to_option
|
494
473
|
|
495
474
|
Returns an `Some` containing the `Right` value or a `None` if this is a `Left`.
|
496
475
|
|
497
476
|
```ruby
|
498
477
|
Right(42).to_option #=> Some(21)
|
499
|
-
Left('undefined').to_option #=> None
|
478
|
+
Left('undefined').to_option #=> None
|
500
479
|
```
|
501
480
|
|
502
481
|
#### Either#any?
|
@@ -615,23 +594,32 @@ It supports two such operations - `flat_map` and `map`. Any class providing them
|
|
615
594
|
is supported by `For`.
|
616
595
|
|
617
596
|
```ruby
|
618
|
-
For(
|
597
|
+
For(Some(2), Some(3)) do |a, b|
|
598
|
+
a * b
|
599
|
+
end #=> Some(6)
|
619
600
|
```
|
620
601
|
|
621
602
|
If one of operands is None, the result is None
|
622
603
|
|
623
604
|
```ruby
|
624
|
-
For(
|
625
|
-
|
605
|
+
For(Some(2), None) do |a, b|
|
606
|
+
a * b
|
607
|
+
end #=> None
|
608
|
+
|
609
|
+
For(None, Some(2)) do |a, b|
|
610
|
+
a * b
|
611
|
+
end #=> None
|
626
612
|
```
|
627
613
|
|
628
614
|
Lets look at first example:
|
629
615
|
|
630
616
|
```ruby
|
631
|
-
For(
|
617
|
+
For(Some(2), None) do |a, b|
|
618
|
+
a * b
|
619
|
+
end #=> None
|
632
620
|
```
|
633
621
|
|
634
|
-
|
622
|
+
it is translated to:
|
635
623
|
|
636
624
|
```ruby
|
637
625
|
Some(2).flat_map do |a|
|
@@ -644,7 +632,7 @@ end
|
|
644
632
|
It works with arrays as well
|
645
633
|
|
646
634
|
```ruby
|
647
|
-
For(
|
635
|
+
For([1, 2], [2, 3], [3, 4]) { |a, b, c| a * b * c }
|
648
636
|
#=> [6, 8, 9, 12, 12, 16, 18, 24]
|
649
637
|
|
650
638
|
```
|
@@ -665,8 +653,9 @@ If you pass lambda as a variable value, it would be evaluated
|
|
665
653
|
only on demand.
|
666
654
|
|
667
655
|
```ruby
|
668
|
-
For(
|
669
|
-
|
656
|
+
For(proc { None }, proc { raise 'kaboom' } ) do |a, b|
|
657
|
+
a * b
|
658
|
+
end #=> None
|
670
659
|
```
|
671
660
|
|
672
661
|
It does not fail since `b` is not evaluated.
|
@@ -675,28 +664,209 @@ You can refer to previously defined variables from within lambdas.
|
|
675
664
|
```ruby
|
676
665
|
maybe_user = find_user('Paul') #=> <#Option value=<#User ...>>
|
677
666
|
|
678
|
-
For(
|
667
|
+
For(maybe_user, ->(user) { user.birthday }) do |user, birthday|
|
679
668
|
"#{user.name} was born on #{birthday}"
|
680
669
|
end #=> Some('Paul was born on 1987-06-17')
|
681
670
|
```
|
682
671
|
|
683
|
-
### Pattern Matching
|
672
|
+
### Pattern Matching (API Documentation)
|
673
|
+
|
674
|
+
Pattern matcher is a combination of partial functions wrapped into nice DSL. Every partial function
|
675
|
+
defined on domain described with guard.
|
676
|
+
|
677
|
+
```ruby
|
678
|
+
pf = Fear.case(Integer) { |x| x / 2 }
|
679
|
+
pf.defined_at?(4) #=> true
|
680
|
+
pf.defined_at?('Foo') #=> false
|
681
|
+
pf.call('Foo') #=> raises Fear::MatchError
|
682
|
+
pf.call_or_else('Foo') { 'not a number' } #=> 'not a number'
|
683
|
+
pf.call_or_else(4) { 'not a number' } #=> 2
|
684
|
+
pf.lift.call('Foo') #=> Fear::None
|
685
|
+
pf.lift.call(4) #=> Fear::Some(2)
|
686
|
+
```
|
687
|
+
|
688
|
+
It uses `#===` method under the hood, so you can pass:
|
684
689
|
|
685
|
-
|
686
|
-
|
687
|
-
|
690
|
+
* Class to check kind of an object.
|
691
|
+
* Lambda to evaluate it against an object.
|
692
|
+
* Any literal, like `4`, `"Foobar"`, etc.
|
693
|
+
* Symbol -- it is converted to lambda using `#to_proc` method.
|
694
|
+
* Qo matcher -- `m.case(Qo[name: 'John']) { .... }`
|
688
695
|
|
696
|
+
Partial functions may be combined with each other:
|
697
|
+
|
698
|
+
```ruby
|
699
|
+
is_even = Fear.case(->(arg) { arg % 2 == 0}) { |arg| "#{arg} is even" }
|
700
|
+
is_odd = Fear.case(->(arg) { arg % 2 == 1}) { |arg| "#{arg} is odd" }
|
701
|
+
|
702
|
+
(10..20).map(&is_even.or_else(is_odd))
|
703
|
+
|
704
|
+
to_integer = Fear.case(String, &:to_i)
|
705
|
+
integer_two_times = Fear.case(Integer) { |x| x * 2 }
|
706
|
+
|
707
|
+
two_times = to_integer.and_then(integer_two_times).or_else(integer_two_times)
|
708
|
+
two_times.(4) #=> 8
|
709
|
+
two_times.('42') #=> 84
|
710
|
+
```
|
711
|
+
|
712
|
+
To create custom pattern match use `Fear.match` method and `case` builder to define
|
713
|
+
branches. For instance this matcher applies different functions to Integers and Strings
|
714
|
+
|
715
|
+
```ruby
|
716
|
+
Fear.match(value) do |m|
|
717
|
+
m.case(Integer) { |n| "#{n} is a number" }
|
718
|
+
m.case(String) { |n| "#{n} is a string" }
|
719
|
+
end
|
720
|
+
```
|
721
|
+
|
722
|
+
if you pass something other than Integer or string, it will raise `Fear::MatchError` error.
|
723
|
+
To avoid raising `MatchError`, you can use `else` method. It defines a branch matching
|
724
|
+
on any value.
|
725
|
+
|
726
|
+
```ruby
|
727
|
+
Fear.match(10..20) do |m|
|
728
|
+
m.case(Integer) { |n| "#{n} is a number" }
|
729
|
+
m.case(String) { |n| "#{n} is a string" }
|
730
|
+
m.else { |n| "#{n} is a #{n.class}" }
|
731
|
+
end #=> "10..20 is a Range"
|
732
|
+
```
|
733
|
+
|
734
|
+
You can use anything as a guardian if it responds to `#===` method:
|
735
|
+
|
689
736
|
```ruby
|
690
|
-
case
|
691
|
-
|
692
|
-
when Some(41) #=> does not match
|
693
|
-
when Some(Fixnum) #=> matches
|
694
|
-
when Some(String) #=> does not match
|
695
|
-
when Some((40..43)) #=> matches
|
696
|
-
when Some(-> (x) { x > 40 }) #=> matches
|
697
|
-
end
|
737
|
+
m.case(20..40) { |m| "#{m} is within range" }
|
738
|
+
m.case(->(x) { x > 10}) { |m| "#{m} is greater than 10" }
|
698
739
|
```
|
699
740
|
|
741
|
+
If you pass a Symbol, it will be converted to proc using `#to_proc` method
|
742
|
+
|
743
|
+
```ruby
|
744
|
+
m.case(:even?) { |x| "#{x} is even" }
|
745
|
+
m.case(:odd?) { |x| "#{x} is odd" }
|
746
|
+
```
|
747
|
+
|
748
|
+
It's also possible to pass several guardians. All should match to pass
|
749
|
+
|
750
|
+
```ruby
|
751
|
+
m.case(Integer, :even?) { |x| ... }
|
752
|
+
m.case(Integer, :odd?) { |x| ... }
|
753
|
+
```
|
754
|
+
|
755
|
+
It's also possible to create matcher and use it several times:
|
756
|
+
|
757
|
+
```ruby
|
758
|
+
matcher = Fear.matcher do |m|
|
759
|
+
m.case(Integer) { |n| "#{n} is a number" }
|
760
|
+
m.case(String) { |n| "#{n} is a string" }
|
761
|
+
m.else { |n| "#{n} is a #{n.class}" }
|
762
|
+
end
|
763
|
+
|
764
|
+
matcher.(42) #=> "42 is a number"
|
765
|
+
matcher.(10..20) #=> "10..20 is a Range"
|
766
|
+
```
|
767
|
+
|
768
|
+
Since matcher is just a syntactic sugar for partial functions, you can combine matchers with partial
|
769
|
+
functions and each other.
|
770
|
+
|
771
|
+
```ruby
|
772
|
+
handle_numbers = Fear.case(Integer, &:itself).and_then(
|
773
|
+
Fear.matcher do |m|
|
774
|
+
m.case(0) { 'zero' }
|
775
|
+
m.case(->(n) { n < 10 }) { 'smaller than ten' }
|
776
|
+
m.case(->(n) { n > 10 }) { 'bigger than ten' }
|
777
|
+
end
|
778
|
+
)
|
779
|
+
|
780
|
+
handle_strings = Fear.case(String, &:itself).and_then(
|
781
|
+
Fear.matcher do |m|
|
782
|
+
m.case('zero') { 0 }
|
783
|
+
m.case('one') { 1 }
|
784
|
+
m.else { 'unexpected' }
|
785
|
+
end
|
786
|
+
)
|
787
|
+
|
788
|
+
handle = handle_numbers.or_else(handle_strings)
|
789
|
+
handle.(0) #=> 'zero'
|
790
|
+
handle.(12) #=> 'bigger than ten'
|
791
|
+
handle.('one') #=> 1
|
792
|
+
```
|
793
|
+
|
794
|
+
#### More examples
|
795
|
+
|
796
|
+
Factorial using pattern matching
|
797
|
+
|
798
|
+
```ruby
|
799
|
+
factorial = Fear.matcher do |m|
|
800
|
+
m.case(->(n) { n <= 1} ) { 1 }
|
801
|
+
m.else { |n| n * factorial.(n - 1) }
|
802
|
+
end
|
803
|
+
|
804
|
+
factorial.(10) #=> 3628800
|
805
|
+
```
|
806
|
+
|
807
|
+
Fibonacci number
|
808
|
+
|
809
|
+
```ruby
|
810
|
+
fibonnaci = Fear.matcher do |m|
|
811
|
+
m.case(0) { 0 }
|
812
|
+
m.case(1) { 1 }
|
813
|
+
m.case(->(n) { n > 1}) { |n| fibonnaci.(n - 1) + fibonnaci.(n - 2) }
|
814
|
+
m.else { raise 'should be positive' }
|
815
|
+
end
|
816
|
+
|
817
|
+
fibonnaci.(10) #=> 55
|
818
|
+
```
|
819
|
+
|
820
|
+
Binary tree set implemented using pattern matching https://gist.github.com/bolshakov/3c51bbf7be95066d55d6d1ac8c605a1d
|
821
|
+
|
822
|
+
#### Monads pattern matching
|
823
|
+
|
824
|
+
You can use `Option#match`, `Either#match`, and `Try#match` method. It performs matching not
|
825
|
+
only on container itself, but on enclosed value as well.
|
826
|
+
|
827
|
+
Pattern match against an `Option`
|
828
|
+
|
829
|
+
```ruby
|
830
|
+
Some(42).match do |m|
|
831
|
+
m.some { |x| x * 2 }
|
832
|
+
m.none { 'none' }
|
833
|
+
end #=> 84
|
834
|
+
```
|
835
|
+
|
836
|
+
pattern match on enclosed value
|
837
|
+
|
838
|
+
```ruby
|
839
|
+
Some(41).match do |m|
|
840
|
+
m.some(:even?) { |x| x / 2 }
|
841
|
+
m.some(:odd?, ->(v) { v > 0 }) { |x| x * 2 }
|
842
|
+
m.none { 'none' }
|
843
|
+
end #=> 82
|
844
|
+
```
|
845
|
+
|
846
|
+
it raises `Fear::MatchError` error if nothing matched. To avoid exception, you can pass `#else` branch
|
847
|
+
|
848
|
+
```ruby
|
849
|
+
Some(42).match do |m|
|
850
|
+
m.some(:odd?) { |x| x * 2 }
|
851
|
+
m.else { 'nothing' }
|
852
|
+
end #=> nothing
|
853
|
+
```
|
854
|
+
|
855
|
+
Pattern matching works the similar way for `Either` and `Try` monads.
|
856
|
+
|
857
|
+
In sake of performance, you may want to generate pattern matching function and reuse it multiple times:
|
858
|
+
|
859
|
+
```ruby
|
860
|
+
matcher = Option.matcher do |m|
|
861
|
+
m.some(42) { 'Yep' }
|
862
|
+
m.some { 'Nope' }
|
863
|
+
m.none { 'Error' }
|
864
|
+
end
|
865
|
+
|
866
|
+
matcher.(Some(42)) #=> 'Yep'
|
867
|
+
matcher.(Some(40)) #=> 'Nope'
|
868
|
+
```
|
869
|
+
|
700
870
|
## Testing
|
701
871
|
|
702
872
|
To simplify testing, you may use [fear-rspec](https://github.com/bolshakov/fear-rspec) gem. It
|