fear 0.10.0 → 0.11.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
[![Build Status](https://travis-ci.org/bolshakov/fear.svg?branch=master)](https://travis-ci.org/bolshakov/fear)
|
3
|
-
[![Maintainability](https://api.codeclimate.com/v1/badges/dbdcfb770918c425e5e4/maintainability)](https://codeclimate.com/github/bolshakov/functional/maintainability)
|
4
|
-
[![Test Coverage](https://api.codeclimate.com/v1/badges/dbdcfb770918c425e5e4/test_coverage)](https://codeclimate.com/github/bolshakov/functional/test_coverage)
|
5
3
|
[![Gem Version](https://badge.fury.io/rb/fear.svg)](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
|