fear 0.0.1

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.
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