fear 0.10.0 → 0.11.0

Sign up to get free protection for your applications and to get access to all the features.
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