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
data/lib/fear/either.rb CHANGED
@@ -19,12 +19,15 @@ module Fear
19
19
  # Left(in)
20
20
  # end
21
21
  #
22
- # puts(
23
- # result.reduce(
24
- # -> (x) { "You passed me the String: #{x}" },
25
- # -> (x) { "You passed me the Int: #{x}, which I will increment. #{x} + 1 = #{x+1}" }
26
- # )
27
- # )
22
+ # result.match do |m|
23
+ # m.right do |x|
24
+ # "You passed me the Int: #{x}, which I will increment. #{x} + 1 = #{x+1}"
25
+ # end
26
+ #
27
+ # m.left do |x|
28
+ # "You passed me the String: #{x}"
29
+ # end
30
+ # end
28
31
  #
29
32
  # Either is right-biased, which means that +Right+ is assumed to be the default case to
30
33
  # operate on. If it is +Left+, operations like +#map+, +#flat_map+, ... return the +Left+ value
@@ -96,14 +99,6 @@ module Fear
96
99
  # Right(42).flat_map { |v| Right(v/2) } #=> Right(21)
97
100
  # Left('undefined').flat_map { |v| Right(v/2) } #=> Left('undefined')
98
101
  #
99
- # @!method to_a
100
- # Returns an +Array+ containing the +Right+ value or an
101
- # empty +Array+ if this is a +Left+.
102
- # @return [Array]
103
- # @example
104
- # Right(42).to_a #=> [21]
105
- # Left('undefined').to_a #=> []
106
- #
107
102
  # @!method to_option
108
103
  # Returns an +Some+ containing the +Right+ value or a +None+ if
109
104
  # this is a +Left+.
@@ -227,6 +222,23 @@ module Fear
227
222
  # Right("daisy").join_left #=> Right("daisy")
228
223
  # Right(Left("daisy")).join_left #=> Right(Left("daisy"))
229
224
  #
225
+ # @!method match(&matcher)
226
+ # Pattern match against this +Either+
227
+ # @yield matcher [Fear::EitherPatternMatch]
228
+ # @example
229
+ # Either(val).match do |m|
230
+ # m.right(Integer) do |x|
231
+ # x * 2
232
+ # end
233
+ #
234
+ # m.right(String) do |x|
235
+ # x.to_i * 2
236
+ # end
237
+ #
238
+ # m.left { |x| x }
239
+ # m.else { 'something unexpected' }
240
+ # end
241
+ #
230
242
  # @see https://github.com/scala/scala/blob/2.12.x/src/library/scala/util/Either.scala
231
243
  #
232
244
  module Either
@@ -249,6 +261,29 @@ module Fear
249
261
  attr_reader :value
250
262
  protected :value
251
263
 
264
+ class << self
265
+ # Build pattern matcher to be used later, despite off
266
+ # +Either#match+ method, id doesn't apply matcher immanently,
267
+ # but build it instead. Unusually in sake of efficiency it's better
268
+ # to statically build matcher and reuse it later.
269
+ #
270
+ # @example
271
+ # matcher =
272
+ # Either.matcher do |m|
273
+ # m.right(Integer, ->(x) { x > 2 }) { |x| x * 2 }
274
+ # m.right(String) { |x| x.to_i * 2 }
275
+ # m.left(String) { :err }
276
+ # m.else { 'error '}
277
+ # end
278
+ # matcher.call(Some(42))
279
+ #
280
+ # @yieldparam [Fear::EitherPatternMatch]
281
+ # @return [Fear::PartialFunction]
282
+ def matcher(&matcher)
283
+ EitherPatternMatch.new(&matcher)
284
+ end
285
+ end
286
+
252
287
  # Include this mixin to access convenient factory methods.
253
288
  # @example
254
289
  # include Fear::Either::Mixin
@@ -0,0 +1,48 @@
1
+ module Fear
2
+ # Either pattern matcher
3
+ #
4
+ # @example
5
+ # pattern_match =
6
+ # EitherPatternMatch.new
7
+ # .right(Integer, ->(x) { x > 2 }) { |x| x * 2 }
8
+ # .right(String) { |x| x.to_i * 2 }
9
+ # .left(String) { :err }
10
+ # .else { 'error '}
11
+ #
12
+ # pattern_match.call(42) => 'NaN'
13
+ #
14
+ # @example the same matcher may be defined using block syntax
15
+ # EitherPatternMatch.new do |m|
16
+ # m.right(Integer, ->(x) { x > 2 }) { |x| x * 2 }
17
+ # m.right(String) { |x| x.to_i * 2 }
18
+ # m.left(String) { :err }
19
+ # m.else { 'error '}
20
+ # end
21
+ #
22
+ # @note it has two optimized subclasses +Fear::LeftPatternMatch+ and +Fear::RightPatternMatch+
23
+ # @api private
24
+ class EitherPatternMatch < Fear::PatternMatch
25
+ LEFT_EXTRACTOR = :left_value.to_proc
26
+ RIGHT_EXTRACTOR = :right_value.to_proc
27
+
28
+ # Match against +Fear::Right+
29
+ #
30
+ # @param conditions [<#==>]
31
+ # @return [Fear::EitherPatternMatch]
32
+ def right(*conditions, &effect)
33
+ branch = Fear.case(Fear::Right, &RIGHT_EXTRACTOR).and_then(Fear.case(*conditions, &effect))
34
+ or_else(branch)
35
+ end
36
+ alias success right
37
+
38
+ # Match against +Fear::Left+
39
+ #
40
+ # @param conditions [<#==>]
41
+ # @return [Fear::EitherPatternMatch]
42
+ def left(*conditions, &effect)
43
+ branch = Fear.case(Fear::Left, &LEFT_EXTRACTOR).and_then(Fear.case(*conditions, &effect))
44
+ or_else(branch)
45
+ end
46
+ alias failure left
47
+ end
48
+ end
@@ -0,0 +1,36 @@
1
+ module Fear
2
+ # Use singleton version of EmptyPartialFunction -- PartialFunction::EMPTY
3
+ # @api private
4
+ module EmptyPartialFunction
5
+ include PartialFunction
6
+
7
+ def defined_at?(_)
8
+ false
9
+ end
10
+
11
+ def call(arg)
12
+ raise MatchError, "partial function not defined at: #{arg}"
13
+ end
14
+
15
+ alias === call
16
+ alias [] call
17
+
18
+ def call_or_else(arg)
19
+ yield arg
20
+ end
21
+
22
+ def or_else(other)
23
+ other
24
+ end
25
+
26
+ def and_then(*)
27
+ self
28
+ end
29
+
30
+ def to_s
31
+ 'Empty partial function'
32
+ end
33
+ end
34
+
35
+ private_constant :EmptyPartialFunction
36
+ end
data/lib/fear/failure.rb CHANGED
@@ -3,6 +3,7 @@ module Fear
3
3
  include Try
4
4
  include Dry::Equalizer(:exception)
5
5
  include RightBiased::Left
6
+ include FailurePatternMatch.mixin
6
7
 
7
8
  # @param [StandardError]
8
9
  def initialize(exception)
@@ -23,13 +24,13 @@ module Fear
23
24
 
24
25
  # @raise
25
26
  def get
26
- fail exception
27
+ raise exception
27
28
  end
28
29
 
29
30
  # @return [Try] of calling block
30
31
  def or_else(*args)
31
32
  super
32
- rescue => error
33
+ rescue StandardError => error
33
34
  Failure.new(error)
34
35
  end
35
36
 
@@ -50,7 +51,7 @@ module Fear
50
51
  yield(exception).tap do |result|
51
52
  Utils.assert_type!(result, Success, Failure)
52
53
  end
53
- rescue => error
54
+ rescue StandardError => error
54
55
  Failure.new(error)
55
56
  end
56
57
 
@@ -59,7 +60,7 @@ module Fear
59
60
  # @return [Try]
60
61
  def recover
61
62
  Success.new(yield(exception))
62
- rescue => error
63
+ rescue StandardError => error
63
64
  Failure.new(error)
64
65
  end
65
66
 
@@ -0,0 +1,8 @@
1
+ module Fear
2
+ # @api private
3
+ class FailurePatternMatch < Fear::TryPatternMatch
4
+ def success(*_conditions)
5
+ self
6
+ end
7
+ end
8
+ end
data/lib/fear/for.rb CHANGED
@@ -4,18 +4,20 @@ module Fear
4
4
  # operations - +flat_map+ and +map+. Any class providing them
5
5
  # is supported by +For+.
6
6
  #
7
- # For(a: Some(2), b: Some(3)) { a * b } #=> Some(6)
7
+ # For(Some(2), Some(3)) do |a, b|
8
+ # a * b
9
+ # end #=> Some(6)
8
10
  #
9
11
  # If one of operands is None, the result is None
10
12
  #
11
- # For(a: Some(2), b: None()) { a * b } #=> None()
12
- # For(a: None(), b: Some(2)) { a * b } #=> None()
13
+ # For(Some(2), None()) { |a, b| a * b } #=> None()
14
+ # For(None(), Some(2)) { |a, b| a * b } #=> None()
13
15
  #
14
16
  # Lets look at first example:
15
17
  #
16
- # For(a: Some(2), b: Some(3)) { a * b }
18
+ # For(Some(2), Some(3)) { |a, b| a * b }
17
19
  #
18
- # would be translated to:
20
+ # it is translated to:
19
21
  #
20
22
  # Some(2).flat_map do |a|
21
23
  # Some(3).map do |b|
@@ -25,10 +27,10 @@ module Fear
25
27
  #
26
28
  # It works with arrays as well
27
29
  #
28
- # For(a: [1, 2], b: [2, 3], c: [3, 4]) { a * b * c }
30
+ # For([1, 2], [2, 3], [3, 4]) { |a, b, c| a * b * c }
29
31
  # #=> [6, 8, 9, 12, 12, 16, 18, 24]
30
32
  #
31
- # would be translated to:
33
+ # it is translated to:
32
34
  #
33
35
  # [1, 2].flat_map do |a|
34
36
  # [2, 3].flat_map do |b|
@@ -38,59 +40,52 @@ module Fear
38
40
  # end
39
41
  # end
40
42
  #
41
- # If you pass lambda as a variable value, it would be evaluated
43
+ # If you pass lambda instead of monad, it would be evaluated
42
44
  # only on demand.
43
45
  #
44
- # For(a: -> { None() }, b: -> { fail 'kaboom' } ) { a * b }
45
- # #=> None()
46
+ # For(proc { None() }, proc { raise 'kaboom' } ) do |a, b|
47
+ # a * b
48
+ # end #=> None()
46
49
  #
47
50
  # It does not fail since `b` is not evaluated.
48
- # You can refer to previously defined variables from within lambdas.
51
+ # You can refer to previously defined monads from within lambdas.
49
52
  #
50
53
  # maybe_user = find_user('Paul') #=> <#Option value=<#User ...>>
51
54
  #
52
- # For(user: maybe_user, birthday: -> { user.birthday }) do
55
+ # For(maybe_user, ->(user) { user.birthday }) do |user, birthday|
53
56
  # "#{user.name} was born on #{birthday}"
54
57
  # end #=> Some('Paul was born on 1987-06-17')
55
58
  #
56
- class For
57
- require_relative 'for/evaluation_context'
59
+ module For
60
+ module_function # rubocop: disable Style/AccessModifierDeclarations
58
61
 
59
- # @param variables [Hash{Symbol => any}]
62
+ # @param monads [<Fear::Option, Fear::Either, Fear::Try, Proc>]
60
63
  #
61
- def initialize(outer_context, **variables)
62
- @variables = variables
63
- @evaluation_context = EvaluationContext.new(outer_context)
64
- end
64
+ def call(monads, inner_values = [], &block)
65
+ head, *tail = *monads
65
66
 
66
- def call(&block)
67
- variable_name_and_monad, *tail = *variables
68
- execute(*variable_name_and_monad, tail, &block)
67
+ if tail.length.zero?
68
+ map(head, inner_values, &block)
69
+ else
70
+ flat_map(head, tail, inner_values, &block)
71
+ end
69
72
  end
70
73
 
71
- private
72
-
73
- attr_reader :variables
74
- attr_reader :evaluation_context
74
+ private def map(head, inner_values)
75
+ resolve(head, inner_values).map do |x|
76
+ yield(*inner_values, x)
77
+ end
78
+ end
75
79
 
76
- def execute(variable_name, monad, monads, &block) # rubocop:disable Metrics/MethodLength
77
- if monads.empty?
78
- resolve(monad).map do |value|
79
- evaluation_context.__assign__(variable_name, value)
80
- evaluation_context.instance_exec(&block)
81
- end
82
- else
83
- resolve(monad).flat_map do |value|
84
- evaluation_context.__assign__(variable_name, value)
85
- variable_name_and_monad, *tail = *monads
86
- execute(*variable_name_and_monad, tail, &block)
87
- end
80
+ private def flat_map(head, tail, inner_values, &block)
81
+ resolve(head, inner_values).flat_map do |x|
82
+ call(tail, inner_values + [x], &block)
88
83
  end
89
84
  end
90
85
 
91
- def resolve(monad_or_proc)
86
+ private def resolve(monad_or_proc, inner_values)
92
87
  if monad_or_proc.respond_to?(:call)
93
- evaluation_context.instance_exec(&monad_or_proc)
88
+ monad_or_proc.call(*inner_values)
94
89
  else
95
90
  monad_or_proc
96
91
  end
@@ -100,29 +95,29 @@ module Fear
100
95
  # @example
101
96
  # include Fear::For::Mixin
102
97
  #
103
- # For(a: Some(2), b: Some(3)) { a * b } #=> Some(6)
104
- # For(a: Some(2), b: None()) { a * b } #=> None()
98
+ # For(Some(2), Some(3)) { |a, b| a * b } #=> Some(6)
99
+ # For(Some(2), None()) { |a, b| a * b } #=> None()
105
100
  #
106
- # For(a: -> { Some(2) }, b: -> { Some(3) }) do
101
+ # For(proc { Some(2) }, proc { Some(3) }) do |a, b|
107
102
  # a * b
108
103
  # end #=> Some(6)
109
104
  #
110
- # For(a: -> { None() }, b: -> { fail }) do
105
+ # For(proc { None() }, proc { raise }) do |a, b|
111
106
  # a * b
112
107
  # end #=> None()
113
108
  #
114
- # For(a: Right(2), b: Right(3)) { a * b } #=> Right(6)
115
- # For(a: Right(2), b: Left(3)) { a * b } #=> Left(3)
109
+ # For(Right(2), Right(3)) { |a, b| a * b } #=> Right(6)
110
+ # For(Right(2), Left(3)) { |a, b| a * b } #=> Left(3)
116
111
  #
117
- # For(a: Success(2), b: Success(3)) { a * b } #=> Success(3)
118
- # For(a: Success(2), b: Failure(...)) { a * b } #=> Failure(...)
112
+ # For(Success(2), Success(3)) { |a| a * b } #=> Success(3)
113
+ # For(Success(2), Failure(...)) { |a, b| a * b } #=> Failure(...)
119
114
  #
120
115
  module Mixin
121
- # @param locals [Hash{Symbol => {#map, #flat_map}}]
116
+ # @param monads [Hash{Symbol => {#map, #flat_map}}]
122
117
  # @return [{#map, #flat_map}]
123
118
  #
124
- def For(**locals, &block)
125
- For.new(self, **locals).call(&block)
119
+ def For(*monads, &block)
120
+ For.call(monads, &block)
126
121
  end
127
122
  end
128
123
  end
data/lib/fear/left.rb CHANGED
@@ -2,6 +2,12 @@ module Fear
2
2
  class Left
3
3
  include Either
4
4
  include RightBiased::Left
5
+ include LeftPatternMatch.mixin
6
+
7
+ # @api private
8
+ def left_value
9
+ value
10
+ end
5
11
 
6
12
  # @return [false]
7
13
  def right?
@@ -37,7 +43,7 @@ module Fear
37
43
 
38
44
  # @param reduce_left [Proc]
39
45
  # @return [any]
40
- def reduce(reduce_left, _)
46
+ def reduce(reduce_left, _reduce_right)
41
47
  reduce_left.call(value)
42
48
  end
43
49
 
@@ -0,0 +1,9 @@
1
+ module Fear
2
+ # @api private
3
+ class LeftPatternMatch < Fear::EitherPatternMatch
4
+ def right(*)
5
+ self
6
+ end
7
+ alias success right
8
+ end
9
+ end
data/lib/fear/none.rb CHANGED
@@ -1,12 +1,14 @@
1
1
  module Fear
2
- class None
2
+ # @api private
3
+ class NoneClass
3
4
  include Option
4
5
  include Dry::Equalizer()
5
6
  include RightBiased::Left
7
+ include NonePatternMatch.mixin
6
8
 
7
9
  # @raise [NoSuchElementError]
8
10
  def get
9
- fail NoSuchElementError
11
+ raise NoSuchElementError
10
12
  end
11
13
 
12
14
  # @return [nil]
@@ -28,5 +30,38 @@ module Fear
28
30
  def reject(*)
29
31
  self
30
32
  end
33
+
34
+ AS_STRING = 'Fear::None'.freeze
35
+
36
+ # @return [String]
37
+ def to_s
38
+ AS_STRING
39
+ end
40
+
41
+ # @return [String]
42
+ def inspect
43
+ AS_STRING
44
+ end
45
+
46
+ # @param other
47
+ # @return [Boolean]
48
+ def ===(other)
49
+ self == other
50
+ end
51
+ end
52
+
53
+ private_constant(:NoneClass)
54
+
55
+ # The only instance of NoneClass
56
+ None = NoneClass.new.freeze
57
+
58
+ class << NoneClass
59
+ def new
60
+ None
61
+ end
62
+
63
+ def inherited
64
+ raise 'you are not allowed to inherit from NoneClass, use Fear::None instead'
65
+ end
31
66
  end
32
67
  end
@@ -0,0 +1,12 @@
1
+ module Fear
2
+ # @api private
3
+ class NonePatternMatch < OptionPatternMatch
4
+ # @param conditions [<#==>]
5
+ # @return [Fear::OptionPatternMatch]
6
+ def some(*_conditions)
7
+ self
8
+ end
9
+ end
10
+
11
+ private_constant :NonePatternMatch
12
+ end