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.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +30 -4
  3. data/.travis.yml +2 -3
  4. data/Appraisals +5 -9
  5. data/CHANGELOG.md +9 -0
  6. data/Gemfile +2 -0
  7. data/LICENSE.txt +1 -1
  8. data/README.md +255 -85
  9. data/Rakefile +393 -0
  10. data/fear.gemspec +13 -6
  11. data/gemfiles/dry_equalizer_0.1.0.gemfile +1 -0
  12. data/gemfiles/dry_equalizer_0.1.0.gemfile.lock +31 -27
  13. data/gemfiles/dry_equalizer_0.2.1.gemfile +1 -0
  14. data/gemfiles/dry_equalizer_0.2.1.gemfile.lock +31 -27
  15. data/lib/fear/either.rb +49 -14
  16. data/lib/fear/either_pattern_match.rb +48 -0
  17. data/lib/fear/empty_partial_function.rb +36 -0
  18. data/lib/fear/failure.rb +5 -4
  19. data/lib/fear/failure_pattern_match.rb +8 -0
  20. data/lib/fear/for.rb +46 -51
  21. data/lib/fear/left.rb +7 -1
  22. data/lib/fear/left_pattern_match.rb +9 -0
  23. data/lib/fear/none.rb +37 -2
  24. data/lib/fear/none_pattern_match.rb +12 -0
  25. data/lib/fear/option.rb +65 -31
  26. data/lib/fear/option_pattern_match.rb +45 -0
  27. data/lib/fear/partial_function/and_then.rb +48 -0
  28. data/lib/fear/partial_function/any.rb +26 -0
  29. data/lib/fear/partial_function/combined.rb +51 -0
  30. data/lib/fear/partial_function/empty.rb +6 -0
  31. data/lib/fear/partial_function/guard/and.rb +36 -0
  32. data/lib/fear/partial_function/guard/and3.rb +39 -0
  33. data/lib/fear/partial_function/guard/or.rb +36 -0
  34. data/lib/fear/partial_function/guard.rb +90 -0
  35. data/lib/fear/partial_function/lifted.rb +20 -0
  36. data/lib/fear/partial_function/or_else.rb +62 -0
  37. data/lib/fear/partial_function.rb +171 -0
  38. data/lib/fear/partial_function_class.rb +26 -0
  39. data/lib/fear/pattern_match.rb +102 -0
  40. data/lib/fear/pattern_matching_api.rb +110 -0
  41. data/lib/fear/right.rb +7 -1
  42. data/lib/fear/right_biased.rb +2 -12
  43. data/lib/fear/right_pattern_match.rb +9 -0
  44. data/lib/fear/some.rb +5 -2
  45. data/lib/fear/some_pattern_match.rb +11 -0
  46. data/lib/fear/success.rb +5 -4
  47. data/lib/fear/success_pattern_match.rb +10 -0
  48. data/lib/fear/try.rb +56 -16
  49. data/lib/fear/try_pattern_match.rb +28 -0
  50. data/lib/fear/utils.rb +24 -14
  51. data/lib/fear/version.rb +1 -1
  52. data/lib/fear.rb +21 -4
  53. data/spec/fear/either_pattern_match_spec.rb +37 -0
  54. data/spec/fear/failure_spec.rb +41 -3
  55. data/spec/fear/for_spec.rb +17 -29
  56. data/spec/fear/guard_spec.rb +101 -0
  57. data/spec/fear/left_spec.rb +38 -0
  58. data/spec/fear/none_spec.rb +80 -0
  59. data/spec/fear/option_pattern_match_spec.rb +35 -0
  60. data/spec/fear/partial_function/empty_spec.rb +36 -0
  61. data/spec/fear/partial_function_and_then_spec.rb +145 -0
  62. data/spec/fear/partial_function_composition_spec.rb +80 -0
  63. data/spec/fear/partial_function_or_else_spec.rb +274 -0
  64. data/spec/fear/partial_function_spec.rb +165 -0
  65. data/spec/fear/pattern_match_spec.rb +59 -0
  66. data/spec/fear/right_biased/left.rb +1 -6
  67. data/spec/fear/right_biased/right.rb +0 -5
  68. data/spec/fear/right_spec.rb +38 -0
  69. data/spec/fear/some_spec.rb +37 -0
  70. data/spec/fear/success_spec.rb +41 -4
  71. data/spec/fear/try_pattern_match_spec.rb +37 -0
  72. metadata +97 -23
  73. data/lib/fear/for/evaluation_context.rb +0 -91
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 9f6f57b15284f2ed3034bc697608d9c44e7401b1
4
- data.tar.gz: 439fb988ae4fab755815100e092a65db332e960e
3
+ metadata.gz: f2a2b3d7163ba41625120e4681b75a845e5d9da9
4
+ data.tar.gz: 5d85fb64e778587e9f8f85695acb4f19602ae47e
5
5
  SHA512:
6
- metadata.gz: 8409d00dd720c5dfbd623e73d72abd86ae5c90235e08dd646ad2e9c19b17c502d1a3d0f9a372ae23bb1af1e6cd9571cd425a6c7305e95256746031a6a634c71b
7
- data.tar.gz: 051a50aa71c6fad65b52798ee303f59037c988365a449afa4653afab5f62d53443c31d0d731d07f09a39c564d330cd927243b701e7925aefa913f78ba7fd52fb
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
- Style/MethodName:
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(2.4.5 2.5.3 2.6.1)
4
- dry_equalizer_versions = %w(0.1.0 0.2.1)
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
@@ -4,3 +4,5 @@ source 'https://rubygems.org'
4
4
  gemspec
5
5
 
6
6
  # gem 'codeclimate-test-reporter', group: :test, require: nil
7
+
8
+ gem 'qo', github: 'baweaver/qo'
data/LICENSE.txt CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2015-2017 Tema Bolshakov
1
+ Copyright (c) 2015-2019 Tema Bolshakov
2
2
 
3
3
  MIT License
4
4
 
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
- name = Option(params[:name])
54
- case name
55
- when Some
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().get_or_else { 24/2 } #=> 12
74
+ None.get_or_else { 24/2 } #=> 12
80
75
 
81
76
  Some(42).get_or_else(12) #=> 42
82
- None().get_or_else(12) #=> 12
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().or_else { Some(21) } #=> Some(21)
92
- None().or_else { None() } #=> 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().include?(17) #=> false
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().each { |value| puts value } #=> does nothing
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().map { |v| v/2 } #=> 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().flat_map { |v| Some(v/2) } #=> 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().any?( |v| v > 10) #=> false
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().select { |v| v < 40 } #=> 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().reject { |v| v < 40 } #=> 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().empty? #=> true
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
- if problem.success?
207
- puts "Result of #{dividend.get} / #{divisor.get} is: #{problem.get}"
208
- else
209
- puts "You must've divided by zero or entered something wrong. Try again"
210
- puts "Info from the exception: #{problem.exception.message}"
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| fail }
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| fail }
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
- puts(
414
- result.reduce(
415
- -> (x) { "You passed me the String: #{x}" },
416
- -> (x) { "You passed me the Int: #{x}, which I will increment. #{x} + 1 = #{x+1}" }
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(a: Some(2), b: Some(3)) { a * b } #=> Some(6)
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(a: Some(2), b: None()) { a * b } #=> None()
625
- For(a: None(), b: Some(2)) { a * b } #=> None()
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(a: Some(2), b: Some(3)) { a * b }
617
+ For(Some(2), None) do |a, b|
618
+ a * b
619
+ end #=> None
632
620
  ```
633
621
 
634
- would be translated to:
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(a: [1, 2], b: [2, 3], c: [3, 4]) { a * b * c }
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(a: -> { None() }, b: -> { fail 'kaboom' } ) { a * b }
669
- #=> None()
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(user: maybe_user, birthday: -> { user.birthday }) do
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
- `Option`, `Either`, and `Try` contains enhanced version of `#===` method. It performs matching not
686
- only on container itself, but on enclosed value as well. I'm writing all the options in a one
687
- case statement in sake of simplicity.
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 Some(42)
691
- when Some(42) #=> matches
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