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.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/.rspec +2 -0
- data/.rubocop.yml +32 -0
- data/.travis.yml +7 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +22 -0
- data/README.md +32 -0
- data/Rakefile +1 -0
- data/fear.gemspec +29 -0
- data/lib/fear/either.rb +145 -0
- data/lib/fear/failure.rb +65 -0
- data/lib/fear/for.rb +104 -0
- data/lib/fear/left.rb +52 -0
- data/lib/fear/none.rb +23 -0
- data/lib/fear/option.rb +112 -0
- data/lib/fear/right.rb +56 -0
- data/lib/fear/right_biased.rb +155 -0
- data/lib/fear/some.rb +42 -0
- data/lib/fear/success.rb +81 -0
- data/lib/fear/try.rb +127 -0
- data/lib/fear/utils.rb +25 -0
- data/lib/fear/version.rb +3 -0
- data/lib/fear.rb +31 -0
- data/spec/fear/failure_spec.rb +78 -0
- data/spec/fear/for_spec.rb +70 -0
- data/spec/fear/left_spec.rb +79 -0
- data/spec/fear/none_spec.rb +37 -0
- data/spec/fear/option_spec.rb +32 -0
- data/spec/fear/right_biased/left.rb +67 -0
- data/spec/fear/right_biased/right.rb +93 -0
- data/spec/fear/right_spec.rb +79 -0
- data/spec/fear/some_spec.rb +48 -0
- data/spec/fear/success_spec.rb +83 -0
- data/spec/fear/utils_spec.rb +60 -0
- data/spec/spec_helper.rb +77 -0
- metadata +175 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 0a628e4319dcc4a672e6a632d6582fdcd9c8bd71
|
4
|
+
data.tar.gz: 3833ced7c33d30fe1111234549b924449a999032
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 56fb816f68a1a85bba4d8819e714c9669b7d2c9972d35a3b4e0c748e16698e2af16687a71cb8099d843800946da2d097fa71045b9c765f8779ef5863ca0a8021
|
7
|
+
data.tar.gz: efa4007136b4b74dbdedddfdfe3e9cd3ccfa5edd4d71d6cd6b554ac0b074298076cdbca2ca3b57a95c81fcf79d4974b9d89a059dd4c5f78855770e100a1134a9
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
inherit_gem:
|
2
|
+
spbtv_code_style: .strict_rubocop.yml
|
3
|
+
|
4
|
+
Style/MethodName:
|
5
|
+
Enabled: false
|
6
|
+
|
7
|
+
# This configuration was generated by `rubocop --auto-gen-config`
|
8
|
+
# on 2015-03-09 00:50:30 +0300 using RuboCop version 0.29.1.
|
9
|
+
# The point is for the user to remove these configuration records
|
10
|
+
# one by one as the offenses are removed from the code base.
|
11
|
+
# Note that changes in the inspected code, or installation of new
|
12
|
+
# versions of RuboCop, may require this file to be generated again.
|
13
|
+
|
14
|
+
# Offense count: 1
|
15
|
+
# Configuration parameters: CountComments.
|
16
|
+
Metrics/ClassLength:
|
17
|
+
Enabled: false
|
18
|
+
Max: 133
|
19
|
+
|
20
|
+
# Offense count: 1
|
21
|
+
# Configuration parameters: CountComments.
|
22
|
+
Metrics/ModuleLength:
|
23
|
+
Max: 130
|
24
|
+
|
25
|
+
# Offense count: 9
|
26
|
+
Style/Documentation:
|
27
|
+
Enabled: false
|
28
|
+
|
29
|
+
# Offense count: 2
|
30
|
+
# Configuration parameters: Methods.
|
31
|
+
Style/SingleLineBlockParams:
|
32
|
+
Enabled: false
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2015 TODO: Write your name
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# Fear
|
2
|
+
[](https://travis-ci.org/bolshakov/fear)
|
3
|
+
|
4
|
+
TODO: Write a gem description
|
5
|
+
|
6
|
+
## Installation
|
7
|
+
|
8
|
+
Add this line to your application's Gemfile:
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
gem 'fear'
|
12
|
+
```
|
13
|
+
|
14
|
+
And then execute:
|
15
|
+
|
16
|
+
$ bundle
|
17
|
+
|
18
|
+
Or install it yourself as:
|
19
|
+
|
20
|
+
$ gem install fear
|
21
|
+
|
22
|
+
## Usage
|
23
|
+
|
24
|
+
TODO: Write usage instructions here
|
25
|
+
|
26
|
+
## Contributing
|
27
|
+
|
28
|
+
1. Fork it ( https://github.com/bolshakov/fear/fork )
|
29
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
30
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
31
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
32
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
data/fear.gemspec
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
|
6
|
+
require 'fear/version'
|
7
|
+
Gem::Specification.new do |spec|
|
8
|
+
spec.name = 'fear'
|
9
|
+
spec.version = Fear::VERSION
|
10
|
+
spec.authors = ['Tema Bolshakov']
|
11
|
+
spec.email = ['abolshakov@spbtv.com']
|
12
|
+
spec.summary = "%q{Ruby port of some Scala's monads.}"
|
13
|
+
spec.description = "Ruby port of some Scala's monads."
|
14
|
+
spec.homepage = 'https://github.com/bolshakov/functional'
|
15
|
+
spec.license = 'MIT'
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0")
|
18
|
+
spec.executables = spec.files.grep(%r{^bin\/}) { |f| File.basename(f) }
|
19
|
+
spec.test_files = spec.files.grep(%r{^spec\/})
|
20
|
+
spec.require_paths = ['lib']
|
21
|
+
|
22
|
+
spec.add_runtime_dependency 'dry-equalizer', '0.2.0'
|
23
|
+
|
24
|
+
spec.add_development_dependency 'bundler', '~> 1.7'
|
25
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
26
|
+
spec.add_development_dependency 'rspec', '~> 3.1'
|
27
|
+
spec.add_development_dependency 'spbtv_code_style'
|
28
|
+
spec.add_development_dependency 'rubocop-rspec'
|
29
|
+
end
|
data/lib/fear/either.rb
ADDED
@@ -0,0 +1,145 @@
|
|
1
|
+
module Fear
|
2
|
+
# Represents a value of one of two possible types (a disjoint union.)
|
3
|
+
# An instance of `Either` is either an instance of `Left` or `Right`.
|
4
|
+
#
|
5
|
+
# A common use of `Either` is as an alternative to `Option` for dealing
|
6
|
+
# with possible missing values. In this usage, `None` is replaced
|
7
|
+
# with a `Left` which can contain useful information.
|
8
|
+
# `Right` takes the place of `Some`. Convention dictates
|
9
|
+
# that `Left` is used for failure and `Right` is used for success.
|
10
|
+
#
|
11
|
+
# For example, you could use `Either<String, Fixnum>` to detect whether a
|
12
|
+
# received input is a `String` or an `Fixnum`.
|
13
|
+
#
|
14
|
+
# @example
|
15
|
+
# in = Readline.readline('Type Either a string or an Int: ', true)
|
16
|
+
# result = begin
|
17
|
+
# Right(Integer(in))
|
18
|
+
# rescue ArgumentError
|
19
|
+
# Left(in)
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# puts(
|
23
|
+
# result.reduce(
|
24
|
+
# -> (x) { "You passed me the Int: #{x}, which I will increment. #{x} + 1 = #{x+1}" },
|
25
|
+
# -> (x) { "You passed me the String: #{x}" }
|
26
|
+
# )
|
27
|
+
# )
|
28
|
+
#
|
29
|
+
# `Either` is right-biased, which means that `Right` is assumed to be the default case to
|
30
|
+
# operate on. If it is `Left`, operations like `#map`, `#flat_map`, ... return the `Left` value
|
31
|
+
# unchanged:
|
32
|
+
#
|
33
|
+
# @example #map
|
34
|
+
# Right(12).map { |_| _ * 2) #=> Right(24)
|
35
|
+
# Left(23).map { |_| _ * 2) #=> Left(23)
|
36
|
+
#
|
37
|
+
# @example #get_or_else
|
38
|
+
# Right(12).get_or_else(17) #=> 12
|
39
|
+
# Left(12).get_or_else(17) #=> 17
|
40
|
+
#
|
41
|
+
# Right(12).get_or_else { 17 } #=> 12
|
42
|
+
# Left(12).get_or_else { 17 } #=> 17
|
43
|
+
#
|
44
|
+
# @example #include?
|
45
|
+
# # Returns true because value of Right is "something" which equals "something".
|
46
|
+
# Right("something").include?("something") #= true
|
47
|
+
#
|
48
|
+
# # Returns false because value of Right is "something" which does not equal "anything".
|
49
|
+
# Right("something").include?("anything") #=> false
|
50
|
+
#
|
51
|
+
# # Returns false because there is no value for Right.
|
52
|
+
# Left("something").include?("something") #=> false
|
53
|
+
#
|
54
|
+
# @example #each
|
55
|
+
# Right(12).each { |x| puts x } # prints "12"
|
56
|
+
# Left(12).each { |x| puts x } # doesn't print
|
57
|
+
#
|
58
|
+
# @example #map
|
59
|
+
# Right('ruby').map(&:length) #=> Right(4)
|
60
|
+
# Left('ruby').map(&:length) #=> Left('ruby')
|
61
|
+
#
|
62
|
+
# @example #flat_map
|
63
|
+
# Right(12).flat_map { |x| Left('ruby') } #=> Left('ruby')
|
64
|
+
# Left(12).flat_map { |x| Left('ruby') } #=> Left(12)
|
65
|
+
#
|
66
|
+
# @example #detect
|
67
|
+
# Right(12).detect(-1, &:even?) #=> Right(12))
|
68
|
+
# Right(7).detect(-1, &:even?) #=> Left(-1)
|
69
|
+
# Left(12).detect(-1, &:even?) #=> Left(-1)
|
70
|
+
# Left(12).detect(-> { -1 }, &:even?) #=> Left(-1)
|
71
|
+
#
|
72
|
+
# @example #to_a
|
73
|
+
# Right(12).to_a #=> [12]
|
74
|
+
# Left(12).to_a #=> []
|
75
|
+
#
|
76
|
+
# @example #to_option
|
77
|
+
# Right(12).to_option #=> Some(12)
|
78
|
+
# Left(12).to_option #=> None()
|
79
|
+
#
|
80
|
+
# @example #any?
|
81
|
+
# Right(12).any? { |v| v > 10 } #=> true
|
82
|
+
# Right(7).any? { |v| v > 10 } #=> false
|
83
|
+
# Left(12).any? { |v| v > 10 } #=> false
|
84
|
+
#
|
85
|
+
# @example #swap
|
86
|
+
# left = Left("left")
|
87
|
+
# right = left.swap #=> Right("left")
|
88
|
+
#
|
89
|
+
# @example #reduce
|
90
|
+
# result = possibly_failing_operation()
|
91
|
+
# log(
|
92
|
+
# result.reduce(
|
93
|
+
# ->(ex) { "Operation failed with #{ex}" },
|
94
|
+
# ->(v) { "Operation produced value: #{v}" },
|
95
|
+
# )
|
96
|
+
# )
|
97
|
+
#
|
98
|
+
# @example #join_right
|
99
|
+
# Right(Right(12)).join_right #=> Right(12)
|
100
|
+
# Right(Left("flower")).join_right #=> Left("flower")
|
101
|
+
# Left("flower").join_right #=> Left("flower")
|
102
|
+
# Left(Right("flower")).join_right #=> Left(Right("flower"))
|
103
|
+
#
|
104
|
+
# @example #join_left
|
105
|
+
# Left(Right("flower")).join_left #=> Right("flower")
|
106
|
+
# Left(Left(12)).join_left #=> Left(12)
|
107
|
+
# Right("daisy").join_left #=> Right("daisy")
|
108
|
+
# Right(Left("daisy")).join_left #=> Right(Left("daisy"))
|
109
|
+
#
|
110
|
+
# @see https://github.com/scala/scala/blob/2.12.x/src/library/scala/util/Either.scala
|
111
|
+
#
|
112
|
+
module Either
|
113
|
+
include Dry::Equalizer(:value)
|
114
|
+
include Fear
|
115
|
+
|
116
|
+
def left_class
|
117
|
+
Left
|
118
|
+
end
|
119
|
+
|
120
|
+
def right_class
|
121
|
+
Right
|
122
|
+
end
|
123
|
+
|
124
|
+
def initialize(value)
|
125
|
+
@value = value
|
126
|
+
end
|
127
|
+
|
128
|
+
attr_reader :value
|
129
|
+
protected :value
|
130
|
+
|
131
|
+
module Mixin
|
132
|
+
# @param [any]
|
133
|
+
# @return [Left]
|
134
|
+
def Left(value)
|
135
|
+
Left.new(value)
|
136
|
+
end
|
137
|
+
|
138
|
+
# @param [any]
|
139
|
+
# @return [Right]
|
140
|
+
def Right(value)
|
141
|
+
Right.new(value)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
data/lib/fear/failure.rb
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
module Fear
|
2
|
+
class Failure
|
3
|
+
include Try
|
4
|
+
include Dry::Equalizer(:value)
|
5
|
+
include RightBiased::Left
|
6
|
+
|
7
|
+
def initialize(exception)
|
8
|
+
@value = exception
|
9
|
+
end
|
10
|
+
|
11
|
+
attr_reader :value
|
12
|
+
protected :value
|
13
|
+
|
14
|
+
def success?
|
15
|
+
false
|
16
|
+
end
|
17
|
+
|
18
|
+
def get
|
19
|
+
fail value
|
20
|
+
end
|
21
|
+
|
22
|
+
# @return [Try] of calling block
|
23
|
+
def or_else
|
24
|
+
Success.new(yield)
|
25
|
+
rescue => error
|
26
|
+
Failure.new(error)
|
27
|
+
end
|
28
|
+
|
29
|
+
# @return [Failure] self
|
30
|
+
def flatten
|
31
|
+
self
|
32
|
+
end
|
33
|
+
|
34
|
+
# @return [Failure] self
|
35
|
+
# TODO: rename to select
|
36
|
+
def detect
|
37
|
+
self
|
38
|
+
end
|
39
|
+
|
40
|
+
# Applies the given block to exception.
|
41
|
+
# This is like `flat_map` for the exception.
|
42
|
+
#
|
43
|
+
# @yieldparam [Exception]
|
44
|
+
# @yieldreturn [Try]
|
45
|
+
# @return [Try]
|
46
|
+
#
|
47
|
+
def recover_with
|
48
|
+
yield(value).tap do |result|
|
49
|
+
Utils.assert_type!(result, Success, Failure)
|
50
|
+
end
|
51
|
+
rescue => error
|
52
|
+
Failure.new(error)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Applies the given block to exception.
|
56
|
+
# This is like map for the exception.
|
57
|
+
# @return [Try]
|
58
|
+
#
|
59
|
+
def recover
|
60
|
+
Success.new(yield(value))
|
61
|
+
rescue => error
|
62
|
+
Failure.new(error)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
data/lib/fear/for.rb
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
module Fear
|
2
|
+
# This class provides syntactic sugar for composition of
|
3
|
+
# multiple monadic operations. It supports two such
|
4
|
+
# operations - `flat_map` and `map`. Any class providing them
|
5
|
+
# is supported by `For`.
|
6
|
+
#
|
7
|
+
# @example Option
|
8
|
+
# For(a: Some(2), b: Some(3)) { a * b } #=> Some(6)
|
9
|
+
# # If one of operands is None, the result is None
|
10
|
+
# For(a: Some(2), b: None()) { a * b } #=> None()
|
11
|
+
# For(a: None(), b: Some(2)) { a * b } #=> None()
|
12
|
+
#
|
13
|
+
# Lets look at first example:
|
14
|
+
#
|
15
|
+
# For(a: Some(2), b: Some(3)) { a * b }
|
16
|
+
#
|
17
|
+
# would be translated to:
|
18
|
+
#
|
19
|
+
# Some(2).flat_map do |a|
|
20
|
+
# Some(3).map do |b|
|
21
|
+
# a * b
|
22
|
+
# end
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
# It works with arrays as well
|
26
|
+
#
|
27
|
+
# For(a: [1, 2], b: [2, 3], c: [3, 4]) { a * b * c }
|
28
|
+
# #=> [6, 8, 9, 12, 12, 16, 18, 24]
|
29
|
+
#
|
30
|
+
# would be translated to:
|
31
|
+
#
|
32
|
+
# [1, 2].flat_map do |a|
|
33
|
+
# [2, 3].flat_map do |b|
|
34
|
+
# [3, 4].map do |c|
|
35
|
+
# a * b * c
|
36
|
+
# end
|
37
|
+
# end
|
38
|
+
# end
|
39
|
+
#
|
40
|
+
class For
|
41
|
+
# Context of block evaluation. It respond to passed locals.
|
42
|
+
#
|
43
|
+
# @example
|
44
|
+
# context = EvaluationContext.new(foo: 'bar')
|
45
|
+
# context.foo #=> 'bar'
|
46
|
+
#
|
47
|
+
class EvaluationContext < BasicObject
|
48
|
+
# @param locals [Hash{Symbol => any}]
|
49
|
+
#
|
50
|
+
def initialize(locals)
|
51
|
+
@locals = locals
|
52
|
+
end
|
53
|
+
|
54
|
+
def method_missing(name, *args, &block)
|
55
|
+
if @locals.include?(name) && args.empty? && block.nil?
|
56
|
+
@locals[name]
|
57
|
+
else
|
58
|
+
super
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def respond_to_missing?(name, _)
|
63
|
+
@locals.key?(name)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
private_constant(:EvaluationContext)
|
67
|
+
|
68
|
+
# @param variables [Hash{Symbol => any}]
|
69
|
+
#
|
70
|
+
def initialize(**variables)
|
71
|
+
@variables = variables
|
72
|
+
end
|
73
|
+
attr_reader :variables
|
74
|
+
|
75
|
+
def call(&block)
|
76
|
+
variable_name_and_monad, *tail = *variables
|
77
|
+
execute({}, *variable_name_and_monad, tail, &block)
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
def execute(locals, variable_name, monad, monads, &block)
|
83
|
+
if monads.empty?
|
84
|
+
monad.map do |value|
|
85
|
+
EvaluationContext.new(locals.merge(variable_name => value)).instance_eval(&block)
|
86
|
+
end
|
87
|
+
else
|
88
|
+
monad.flat_map do |value|
|
89
|
+
variable_name_and_monad, *tail = *monads
|
90
|
+
execute(locals.merge(variable_name => value), *variable_name_and_monad, tail, &block)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
module Mixin
|
96
|
+
# @param locals [Hash{Symbol => {#map, #flat_map}}]
|
97
|
+
# @return [{#map, #flat_map}]
|
98
|
+
#
|
99
|
+
def For(**locals, &block)
|
100
|
+
For.new(**locals).call(&block)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
data/lib/fear/left.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
module Fear
|
2
|
+
class Left
|
3
|
+
include Either
|
4
|
+
include RightBiased::Left
|
5
|
+
|
6
|
+
# Returns `Left` or `default`.
|
7
|
+
#
|
8
|
+
# @param default [Proc, any]
|
9
|
+
# @return [Either]
|
10
|
+
#
|
11
|
+
def detect(default)
|
12
|
+
Left.new(Utils.return_or_call_proc(default))
|
13
|
+
end
|
14
|
+
|
15
|
+
# @return [Right] value in `Right`
|
16
|
+
#
|
17
|
+
def swap
|
18
|
+
Right.new(value)
|
19
|
+
end
|
20
|
+
|
21
|
+
# @param reduce_left [Proc] the function to apply if this is a `Left`
|
22
|
+
# @return [any] Applies `reduce_left` to the value.
|
23
|
+
#
|
24
|
+
def reduce(reduce_left, _)
|
25
|
+
reduce_left.call(value)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Joins an `Either` through `Right`.
|
29
|
+
#
|
30
|
+
# @return [self]
|
31
|
+
#
|
32
|
+
def join_right
|
33
|
+
self
|
34
|
+
end
|
35
|
+
|
36
|
+
# Joins an `Either` through `Left`.
|
37
|
+
#
|
38
|
+
# This method requires that the left side of this `Either` is itself an
|
39
|
+
# Either type.
|
40
|
+
#
|
41
|
+
# This method, and `join_right`, are analogous to `Option#flatten`
|
42
|
+
#
|
43
|
+
# @return [Either]
|
44
|
+
# @raise [TypeError] if it does not contain `Either`.
|
45
|
+
#
|
46
|
+
def join_left
|
47
|
+
value.tap do |v|
|
48
|
+
Utils.assert_type!(v, Either)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
data/lib/fear/none.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
module Fear
|
2
|
+
class None
|
3
|
+
include Option
|
4
|
+
include Dry::Equalizer()
|
5
|
+
include RightBiased::Left
|
6
|
+
|
7
|
+
# Ignores the given block and return self.
|
8
|
+
#
|
9
|
+
# @return [None]
|
10
|
+
#
|
11
|
+
def detect(*)
|
12
|
+
self
|
13
|
+
end
|
14
|
+
|
15
|
+
# Ignores the given block and return self.
|
16
|
+
#
|
17
|
+
# @return [None]
|
18
|
+
#
|
19
|
+
def reject(*)
|
20
|
+
self
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/lib/fear/option.rb
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
module Fear
|
2
|
+
# Represents optional values. Instances of `Option`
|
3
|
+
# are either an instance of `Some` or the object `None`.
|
4
|
+
#
|
5
|
+
# The most idiomatic way to use an `Option` instance is to treat it
|
6
|
+
# as a collection or monad and use `map`, `flat_map`, `detect`, or `each`:
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
# name = Option(params[:name])
|
10
|
+
# upper = name.map(&:strip).detect { |n| n.length != 0 }.map(&:upcase)
|
11
|
+
# puts upper.get_or_else('')
|
12
|
+
#
|
13
|
+
# This allows for sophisticated chaining of `Option` values without
|
14
|
+
# having to check for the existence of a value.
|
15
|
+
#
|
16
|
+
# A less-idiomatic way to use `Option` values is via pattern matching:
|
17
|
+
#
|
18
|
+
# @example
|
19
|
+
# name = Option(params[:name])
|
20
|
+
# case name
|
21
|
+
# when Some
|
22
|
+
# puts name.strip.upcase
|
23
|
+
# when None
|
24
|
+
# puts 'No name value'
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
# or manually checking for non emptiness:
|
28
|
+
#
|
29
|
+
# @example
|
30
|
+
# name = Option(params[:name])
|
31
|
+
# if name.empty?
|
32
|
+
# puts 'No name value'
|
33
|
+
# else
|
34
|
+
# puts name.strip.upcase
|
35
|
+
# end
|
36
|
+
#
|
37
|
+
# @example #detect
|
38
|
+
# User.find(params[:id]).detect do |user|
|
39
|
+
# user.posts.count > 0
|
40
|
+
# end #=> Some(User)
|
41
|
+
#
|
42
|
+
# User.find(params[:id]).detect do |user|
|
43
|
+
# user.posts.count > 0
|
44
|
+
# end #=> None
|
45
|
+
#
|
46
|
+
# User.find(params[:id])
|
47
|
+
# .detect(&:confirmed?)
|
48
|
+
# .map(&:posts)
|
49
|
+
# .inject(0, &:count)
|
50
|
+
#
|
51
|
+
# @example #reject
|
52
|
+
# Some(42).reject { |v| v > 0 } #=> None
|
53
|
+
# Some(42).reject { |v| v < 0 } #=> Some(42)
|
54
|
+
# None().reject { |v| v > 0 } #=> None
|
55
|
+
#
|
56
|
+
# @see https://github.com/scala/scala/blob/2.11.x/src/library/scala/Option.scala
|
57
|
+
#
|
58
|
+
module Option
|
59
|
+
def left_class
|
60
|
+
None
|
61
|
+
end
|
62
|
+
|
63
|
+
def right_class
|
64
|
+
Some
|
65
|
+
end
|
66
|
+
|
67
|
+
# @return [true, false] true if the option is `None`,
|
68
|
+
# false otherwise.
|
69
|
+
#
|
70
|
+
def empty?
|
71
|
+
is_a?(None)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Returns the option's value if it is nonempty,
|
75
|
+
# or `nil` if it is empty. Useful for unwrapping
|
76
|
+
# option's value.
|
77
|
+
#
|
78
|
+
# @return [Object, nil] the option's value if it is
|
79
|
+
# nonempty or `nil` if it is empty
|
80
|
+
#
|
81
|
+
# @example
|
82
|
+
# Option(24).or_nil #=> 24
|
83
|
+
# Option(nil).or_nil #=> nil
|
84
|
+
#
|
85
|
+
def or_nil
|
86
|
+
get_or_else { nil }
|
87
|
+
end
|
88
|
+
|
89
|
+
module Mixin
|
90
|
+
# @param value [any]
|
91
|
+
# @return [Some, None]
|
92
|
+
def Option(value)
|
93
|
+
if value.nil?
|
94
|
+
None()
|
95
|
+
else
|
96
|
+
Some(value)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# @return [None]
|
101
|
+
def None
|
102
|
+
None.new
|
103
|
+
end
|
104
|
+
|
105
|
+
# @param value [any] except nil
|
106
|
+
# @return [None]
|
107
|
+
def Some(value)
|
108
|
+
Some.new(value)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|