fear 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/lib/fear/right.rb ADDED
@@ -0,0 +1,56 @@
1
+ module Fear
2
+ class Right
3
+ include Either
4
+ include RightBiased::Right
5
+
6
+ # Returns `Left(default)` if the given predicate
7
+ # does not hold for the right value, otherwise, returns `Right`.
8
+ #
9
+ # @param default [Proc, any]
10
+ # @return [Either]
11
+ #
12
+ def detect(default)
13
+ if yield(value)
14
+ self
15
+ else
16
+ Left.new(Utils.return_or_call_proc(default))
17
+ end
18
+ end
19
+
20
+ # @return [Left] value in `Left`
21
+ def swap
22
+ Left.new(value)
23
+ end
24
+
25
+ # @param reduce_right [Proc] the function to apply if this is a `Right`
26
+ # @return [any] Applies `reduce_right` to the value.
27
+ #
28
+ def reduce(_, reduce_right)
29
+ reduce_right.call(value)
30
+ end
31
+
32
+ # Joins an `Either` through `Right`.
33
+ #
34
+ # This method requires that the right side of this `Either` is itself an
35
+ # Either type.
36
+ #
37
+ # This method, and `join_left`, are analogous to `Option#flatten`
38
+ #
39
+ # @return [Either]
40
+ # @raise [TypeError] if it does not contain `Either`.
41
+ #
42
+ def join_right
43
+ value.tap do |v|
44
+ Utils.assert_type!(v, Either)
45
+ end
46
+ end
47
+
48
+ # Joins an `Either` through `Left`.
49
+ #
50
+ # @return [self]
51
+ #
52
+ def join_left
53
+ self
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,155 @@
1
+ module Fear
2
+ module RightBiased
3
+ # Performs necessary interface and type checks.
4
+ #
5
+ module Interface
6
+ # Returns the value from this `RightBiased::Right` or the given argument if
7
+ # this is a `RightBiased::Left`.
8
+ def get_or_else(*args, &block)
9
+ Utils.assert_arg_or_block!('get_or_else', *args, &block)
10
+ super
11
+ end
12
+
13
+ def flat_map
14
+ super.tap do |result|
15
+ Utils.assert_type!(result, left_class, right_class)
16
+ end
17
+ end
18
+
19
+ # Ensures that returned value either left, or right.
20
+ def detect(*)
21
+ super.tap do |result|
22
+ Utils.assert_type!(result, left_class, right_class)
23
+ end
24
+ end
25
+ end
26
+
27
+ module Right
28
+ class << self
29
+ def included(base)
30
+ base.prepend Interface
31
+ end
32
+ end
33
+
34
+ # @!method get_or_else(default)
35
+ # @param default [any]
36
+ # @return [any] the `#value`.
37
+ #
38
+ # @!method get_or_else
39
+ # @return [any] the `#value`.
40
+ #
41
+ def get_or_else(*_args)
42
+ value
43
+ end
44
+
45
+ # @param [any]
46
+ # @return [Boolean] `true` if it has an element that is equal
47
+ # (as determined by `==`) to `other_value`, `false` otherwise.
48
+ #
49
+ def include?(other_value)
50
+ value == other_value
51
+ end
52
+
53
+ # Executes the given side-effecting block.
54
+ #
55
+ # @return [self]
56
+ #
57
+ def each
58
+ yield(value)
59
+ self
60
+ end
61
+
62
+ # Maps the value using given block.
63
+ #
64
+ # @return [RightBiased::Right]
65
+ #
66
+ def map
67
+ self.class.new(yield(value))
68
+ end
69
+
70
+ # Binds the given function across `RightBiased::Right`.
71
+ #
72
+ # @return [RightBiased::Left, RightBiased::Right]
73
+ #
74
+ def flat_map
75
+ yield(value)
76
+ end
77
+
78
+ # @return [Array] containing value
79
+ def to_a
80
+ [value]
81
+ end
82
+
83
+ # @return [Option] containing value
84
+ def to_option
85
+ Some.new(value)
86
+ end
87
+
88
+ # @return [Boolean] true if value satisfies predicate.
89
+ def any?
90
+ yield(value)
91
+ end
92
+ end
93
+
94
+ module Left
95
+ prepend Interface
96
+ include Utils
97
+ # @!method get_or_else(default)
98
+ # @param default [any]
99
+ # @return [any] default value
100
+ #
101
+ # @!method get_or_else
102
+ # @return [any] result of evaluating a block.
103
+ #
104
+ def get_or_else(*args)
105
+ args.fetch(0) { yield }
106
+ end
107
+
108
+ # @param [any]
109
+ # @return [false]
110
+ #
111
+ def include?(_)
112
+ false
113
+ end
114
+
115
+ # Ignores the given side-effecting block and return self.
116
+ #
117
+ # @return [RightBiased::Left]
118
+ #
119
+ def each
120
+ self
121
+ end
122
+
123
+ # Ignores the given block and return self.
124
+ #
125
+ # @return [RightBiased::Left]
126
+ #
127
+ def map
128
+ self
129
+ end
130
+
131
+ # Ignores the given block and return self.
132
+ #
133
+ # @return [RightBiased::Left]
134
+ #
135
+ def flat_map
136
+ self
137
+ end
138
+
139
+ # @return [Array] empty array
140
+ def to_a
141
+ []
142
+ end
143
+
144
+ # @return [None]
145
+ def to_option
146
+ None.new
147
+ end
148
+
149
+ # @return [false]
150
+ def any?
151
+ false
152
+ end
153
+ end
154
+ end
155
+ end
data/lib/fear/some.rb ADDED
@@ -0,0 +1,42 @@
1
+ module Fear
2
+ class Some
3
+ include Option
4
+ include Dry::Equalizer(:get)
5
+ include RightBiased::Right
6
+
7
+ attr_reader :value
8
+ protected :value
9
+
10
+ def initialize(value)
11
+ @value = value
12
+ end
13
+
14
+ # @return option's value
15
+ def get
16
+ @value
17
+ end
18
+
19
+ # @return [Option] self if this `Option` is nonempty and
20
+ # applying the `predicate` to this option's value
21
+ # returns true. Otherwise, return `None`.
22
+ #
23
+ def detect
24
+ if yield(value)
25
+ self
26
+ else
27
+ None.new
28
+ end
29
+ end
30
+
31
+ # @return [Option] if applying the `predicate` to this
32
+ # option's value returns false. Otherwise, return `None`.
33
+ #
34
+ def reject
35
+ if yield(value)
36
+ None.new
37
+ else
38
+ self
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,81 @@
1
+ module Fear
2
+ class Success
3
+ include Try
4
+ include Dry::Equalizer(:value)
5
+ include RightBiased::Right
6
+
7
+ attr_reader :value
8
+ protected :value
9
+
10
+ def initialize(value)
11
+ @value = value
12
+ end
13
+
14
+ def get
15
+ @value
16
+ end
17
+
18
+ def success?
19
+ true
20
+ end
21
+
22
+ # @return [Success] self
23
+ def or_else
24
+ self
25
+ end
26
+
27
+ # Transforms a nested `Try`, ie, a `Success` of `Success``,
28
+ # into an un-nested `Try`, ie, a `Success`.
29
+ # @return [Try]
30
+ #
31
+ def flatten
32
+ if value.is_a?(Try)
33
+ value.flatten
34
+ else
35
+ self
36
+ end
37
+ end
38
+
39
+ # Converts this to a `Failure` if the predicate
40
+ # is not satisfied.
41
+ # @yieldparam [any] value
42
+ # @yieldreturn [Boolean]
43
+ # @return [Try]
44
+ #
45
+ def detect
46
+ if yield(value)
47
+ self
48
+ else
49
+ fail NoSuchElementError, "Predicate does not hold for `#{value}`"
50
+ end
51
+ rescue => error
52
+ Failure.new(error)
53
+ end
54
+
55
+ # @return [Success] self
56
+ #
57
+ def recover_with
58
+ self
59
+ end
60
+
61
+ # @return [Success] self
62
+ #
63
+ def recover
64
+ self
65
+ end
66
+
67
+ # @return [Try]
68
+ def map
69
+ super
70
+ rescue => error
71
+ Failure.new(error)
72
+ end
73
+
74
+ # @return [Try]
75
+ def flat_map
76
+ super
77
+ rescue => error
78
+ Failure.new(error)
79
+ end
80
+ end
81
+ end
data/lib/fear/try.rb ADDED
@@ -0,0 +1,127 @@
1
+ module Fear
2
+ # The `Try` represents a computation that may either result
3
+ # in an exception, or return a successfully computed value.
4
+ #
5
+ # Instances of `Try`, are either an instance of `Success` or
6
+ # `Failure`.
7
+ #
8
+ # For example, `Try` can be used to perform division on a
9
+ # user-defined input, without the need to do explicit
10
+ # exception-handling in all of the places that an exception
11
+ # might occur.
12
+ #
13
+ # @example
14
+ # dividend = Try { params[:dividend].to_i }
15
+ # divisor = Try { params[:divisor].to_i }
16
+ # problem = dividend.flat_map { |x| divisor.map { |y| x / y }
17
+ #
18
+ # if problem.success?
19
+ # puts "Result of #{dividend.get} / #{divisor.get} is: #{problem.get}"
20
+ # else
21
+ # puts "You must've divided by zero or entered something wrong. Try again"
22
+ # puts "Info from the exception: #{problem.exception.message}"
23
+ # end
24
+ #
25
+ # An important property of `Try` shown in the above example is its
26
+ # ability to `pipeline`, or chain, operations, catching exceptions
27
+ # along the way. The `flat_map` and `map` combinators in the above
28
+ # example each essentially pass off either their successfully completed
29
+ # value, wrapped in the `Success` type for it to be further operated
30
+ # upon by the next combinator in the chain, or the exception wrapped
31
+ # in the `Failure` type usually to be simply passed on down the chain.
32
+ # Combinators such as `rescue` and `recover` are designed to provide some
33
+ # type of default behavior in the case of failure.
34
+ #
35
+ # @note only non-fatal exceptions are caught by the combinators on `Try`.
36
+ # Serious system errors, on the other hand, will be thrown.
37
+ #
38
+ # @note all `Try` combinators will catch exceptions and return failure
39
+ # unless otherwise specified in the documentation.
40
+ #
41
+ # @example #or_else
42
+ # Success(42).or_else { -1 } #=> Success(42)
43
+ # Failure(ArgumentError.new).or_else { -1 } #=> Success(-1)
44
+ # Failure(ArgumentError.new).or_else { 1/0 } #=> Failure(ZeroDivisionError.new('divided by 0'))
45
+ #
46
+ # @example #flatten
47
+ # Success(42).flatten #=> Success(42)
48
+ # Success(Success(42)).flatten #=> Success(42)
49
+ # Success(Failure(ArgumentError.new)).flatten #=> Failure(ArgumentError.new)
50
+ # Failure(ArgumentError.new).flatten { -1 } #=> Failure(ArgumentError.new)
51
+ #
52
+ # @example #map
53
+ # Success(42).map { |v| v/2 } #=> Success(21)
54
+ # Failure(ArgumentError.new).map { |v| v/2 } #=> Failure(ArgumentError.new)
55
+ #
56
+ # @example #detect
57
+ # Success(42).detect { |v| v > 40 }
58
+ # #=> Success(21)
59
+ # Success(42).detect { |v| v < 40 }
60
+ # #=> Failure(NoSuchElementError.new("Predicate does not hold for 42"))
61
+ # Failure(ArgumentError.new).detect { |v| v < 40 }
62
+ # #=> Failure(ArgumentError.new)
63
+ #
64
+ # @example #recover_with
65
+ # Success(42).recover_with { |e| Success(e.massage) }
66
+ # #=> Success(42)
67
+ # Failure(ArgumentError.new).recover_with { |e| Success(e.massage) }
68
+ # #=> Success('ArgumentError')
69
+ # Failure(ArgumentError.new).recover_with { |e| fail }
70
+ # #=> Failure(RuntimeError)
71
+ #
72
+ # @example #recover
73
+ # Success(42).recover { |e| e.massage }
74
+ # #=> Success(42)
75
+ # Failure(ArgumentError.new).recover { |e| e.massage }
76
+ # #=> Success('ArgumentError')
77
+ # Failure(ArgumentError.new).recover { |e| fail }
78
+ # #=> Failure(RuntimeError)
79
+ #
80
+ # @author based on Twitter's original implementation.
81
+ # @see https://github.com/scala/scala/blob/2.11.x/src/library/scala/util/Try.scala
82
+ #
83
+ module Try
84
+ def left_class
85
+ Failure
86
+ end
87
+
88
+ def right_class
89
+ Success
90
+ end
91
+
92
+ # @return [true, false] `true` if the `Try` is a `Failure`,
93
+ # `false` otherwise.
94
+ #
95
+ def failure?
96
+ !success?
97
+ end
98
+
99
+ module Mixin
100
+ # Constructs a `Try` using the block. This
101
+ # method will ensure any non-fatal exception is caught and a
102
+ # `Failure` object is returned.
103
+ # @return [Try]
104
+ #
105
+ def Try
106
+ Success.new(yield)
107
+ rescue StandardError => error
108
+ Failure.new(error)
109
+ end
110
+
111
+ # @param exception [StandardError]
112
+ # @return [Failure]
113
+ #
114
+ def Failure(exception)
115
+ fail TypeError, "not an error: #{exception}" unless exception.is_a?(StandardError)
116
+ Failure.new(exception)
117
+ end
118
+
119
+ # @param value [any]
120
+ # @return [Success]
121
+ #
122
+ def Success(value)
123
+ Success.new(value)
124
+ end
125
+ end
126
+ end
127
+ end
data/lib/fear/utils.rb ADDED
@@ -0,0 +1,25 @@
1
+ module Fear
2
+ module Utils
3
+ extend self
4
+
5
+ def assert_arg_or_block!(method_name, *args)
6
+ unless block_given? ^ args.any?
7
+ fail ArgumentError, "##{method_name} accepts either one argument or block"
8
+ end
9
+ end
10
+
11
+ def assert_type!(value, *types)
12
+ if types.none? { |type| value.is_a?(type) }
13
+ fail TypeError, "expected `#{value}` to be of #{types.join(', ')} class"
14
+ end
15
+ end
16
+
17
+ def return_or_call_proc(value)
18
+ if value.respond_to?(:call)
19
+ value.call
20
+ else
21
+ value
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,3 @@
1
+ module Fear
2
+ VERSION = '0.0.1'.freeze
3
+ end
data/lib/fear.rb ADDED
@@ -0,0 +1,31 @@
1
+ require 'dry-equalizer'
2
+ require 'fear/version'
3
+
4
+ module Fear
5
+ Error = Class.new(StandardError)
6
+ IllegalStateException = Class.new(Error)
7
+ NoSuchElementError = Class.new(Error)
8
+
9
+ autoload :For, 'fear/for'
10
+ autoload :Utils, 'fear/utils'
11
+ autoload :RightBiased, 'fear/right_biased'
12
+
13
+ autoload :Option, 'fear/option'
14
+ autoload :Some, 'fear/some'
15
+ autoload :None, 'fear/none'
16
+
17
+ autoload :Try, 'fear/try'
18
+ autoload :Success, 'fear/success'
19
+ autoload :Failure, 'fear/failure'
20
+
21
+ autoload :Either, 'fear/either'
22
+ autoload :Left, 'fear/left'
23
+ autoload :Right, 'fear/right'
24
+
25
+ module Mixin
26
+ include Either::Mixin
27
+ include For::Mixin
28
+ include Option::Mixin
29
+ include Try::Mixin
30
+ end
31
+ end
@@ -0,0 +1,78 @@
1
+ RSpec.describe Fear::Failure do
2
+ let(:failure) { described_class.new(RuntimeError.new('error')) }
3
+
4
+ it_behaves_like Fear::RightBiased::Left do
5
+ let(:left) { failure }
6
+ end
7
+
8
+ describe '#success?' do
9
+ subject { failure }
10
+ it { is_expected.not_to be_success }
11
+ end
12
+
13
+ describe '#get' do
14
+ subject { proc { failure.get } }
15
+ it { is_expected.to raise_error(RuntimeError, 'error') }
16
+ end
17
+
18
+ describe '#or_else' do
19
+ context 'default does not fail' do
20
+ subject { failure.or_else { 'value' } }
21
+ it { is_expected.to eq(Fear::Success.new('value')) }
22
+ end
23
+
24
+ context 'default fails with error' do
25
+ subject(:or_else) { failure.or_else { fail 'unexpected error' } }
26
+ it { is_expected.to be_kind_of(described_class) }
27
+ it { expect { or_else.get }.to raise_error(RuntimeError, 'unexpected error') }
28
+ end
29
+ end
30
+
31
+ describe '#flatten' do
32
+ subject { failure.flatten }
33
+ it { is_expected.to eq(failure) }
34
+ end
35
+
36
+ describe '#detect' do
37
+ subject { failure.detect { |v| v == 'value' } }
38
+ it { is_expected.to eq(failure) }
39
+ end
40
+
41
+ context '#recover_with' do
42
+ context 'block does not fail' do
43
+ subject do
44
+ failure.recover_with do |error|
45
+ Fear::Success.new(error.message)
46
+ end
47
+ end
48
+
49
+ it 'returns result of evaluation of the block against the error' do
50
+ is_expected.to eq(Fear::Success.new('error'))
51
+ end
52
+ end
53
+
54
+ context 'block fails' do
55
+ subject(:recover_with) { failure.recover_with { fail 'unexpected error' } }
56
+
57
+ it { is_expected.to be_kind_of(described_class) }
58
+ it { expect { recover_with.get }.to raise_error(RuntimeError, 'unexpected error') }
59
+ end
60
+ end
61
+
62
+ context '#recover' do
63
+ context 'block does not fail' do
64
+ subject { failure.recover(&:message) }
65
+
66
+ it 'returns Success of evaluation of the block against the error' do
67
+ is_expected.to eq(Fear::Success.new('error'))
68
+ end
69
+ end
70
+
71
+ context 'block fails' do
72
+ subject(:recover) { failure.recover { fail 'unexpected error' } }
73
+
74
+ it { is_expected.to be_kind_of(described_class) }
75
+ it { expect { recover.get }.to raise_error(RuntimeError, 'unexpected error') }
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,70 @@
1
+ RSpec.describe Fear::For do
2
+ include Fear::For::Mixin
3
+
4
+ context 'unary' do
5
+ context 'Some' do
6
+ subject do
7
+ For(a: Fear::Some.new(2)) { a * 2 }
8
+ end
9
+
10
+ it { is_expected.to eq(Fear::Some.new(4)) }
11
+ end
12
+
13
+ context 'None' do
14
+ subject do
15
+ For(a: Fear::None.new) { a * 2 }
16
+ end
17
+
18
+ it { is_expected.to eq(Fear::None.new) }
19
+ end
20
+ end
21
+
22
+ context 'arrays' do
23
+ subject do
24
+ For(a: [1, 2], b: [2, 3], c: [3, 4]) do
25
+ a * b * c
26
+ end
27
+ end
28
+ it { is_expected.to eq([6, 8, 9, 12, 12, 16, 18, 24]) }
29
+ end
30
+
31
+ context 'ternary' do
32
+ subject do
33
+ For(a: first, b: second, c: third) do
34
+ a * b * c
35
+ end
36
+ end
37
+
38
+ context 'all Same' do
39
+ let(:first) { Fear::Some.new(2) }
40
+ let(:second) { Fear::Some.new(3) }
41
+ let(:third) { Fear::Some.new(4) }
42
+
43
+ it { is_expected.to eq(Fear::Some.new(24)) }
44
+ end
45
+
46
+ context 'first None' do
47
+ let(:first) { Fear::None.new }
48
+ let(:second) { Fear::Some.new(3) }
49
+ let(:third) { Fear::Some.new(4) }
50
+
51
+ it { is_expected.to eq(Fear::None.new) }
52
+ end
53
+
54
+ context 'second None' do
55
+ let(:first) { Fear::Some.new(2) }
56
+ let(:second) { Fear::None.new }
57
+ let(:third) { Fear::Some.new(4) }
58
+
59
+ it { is_expected.to eq(Fear::None.new) }
60
+ end
61
+
62
+ context 'last None' do
63
+ let(:first) { Fear::Some.new(2) }
64
+ let(:second) { Fear::Some.new(3) }
65
+ let(:third) { Fear::None.new }
66
+
67
+ it { is_expected.to eq(Fear::None.new) }
68
+ end
69
+ end
70
+ end