fear 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Build Status](https://travis-ci.org/bolshakov/fear.svg?branch=master)](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
|