dry-monads 1.2.0 → 1.3.0
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 +4 -4
- data/.travis.yml +14 -12
- data/CHANGELOG.md +85 -0
- data/Gemfile +1 -0
- data/README.md +4 -4
- data/dry-monads.gemspec +1 -1
- data/lib/dry/monads.rb +2 -1
- data/lib/dry/monads/do.rb +105 -111
- data/lib/dry/monads/do/all.rb +26 -2
- data/lib/dry/monads/do/mixin.rb +56 -0
- data/lib/dry/monads/errors.rb +10 -0
- data/lib/dry/monads/list.rb +38 -0
- data/lib/dry/monads/maybe.rb +89 -2
- data/lib/dry/monads/result.rb +90 -2
- data/lib/dry/monads/result/fixed.rb +2 -1
- data/lib/dry/monads/right_biased.rb +40 -0
- data/lib/dry/monads/task.rb +6 -2
- data/lib/dry/monads/try.rb +9 -4
- data/lib/dry/monads/validated.rb +5 -1
- data/lib/dry/monads/version.rb +2 -2
- metadata +5 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3b5983b622b49edc1008fc8717adf6f4150f011c55ca094879f6788cfed657f2
|
4
|
+
data.tar.gz: 8e01a982d197dbc26e82df01c804dba93e8aa327aeb9cb248a07eeda7a7c636c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f81e00f7455a03b12996724fd35404d81dff4cbc934e1868a2329c26cbaae5cb7f5edcde6c9766f754844e8ce0b839d5ca10781e0c1c08702020e938956703b3
|
7
|
+
data.tar.gz: 4ea9d2ffa5988996517ba096ef8985331a213fadbdb6d81b2597012a0d7ada3da3716422ba155accd0a9932bf5802a905a3b19162526c5f2a501f45e0b46537e
|
data/.travis.yml
CHANGED
@@ -1,26 +1,28 @@
|
|
1
1
|
language: ruby
|
2
2
|
dist: trusty
|
3
|
-
sudo: false
|
4
3
|
cache: bundler
|
5
4
|
bundler_args: --without benchmarks docs
|
6
5
|
script:
|
7
|
-
- bundle exec
|
6
|
+
- bundle exec rspec spec $RSPEC_OPTS
|
8
7
|
before_install:
|
9
8
|
- gem update --system
|
10
9
|
- gem install bundler
|
11
10
|
after_success:
|
12
|
-
-
|
11
|
+
- "[ -d coverage ] && bundle exec codeclimate-test-reporter"
|
13
12
|
rvm:
|
14
|
-
- 2.
|
15
|
-
- 2.
|
16
|
-
- 2.
|
17
|
-
- 2.
|
18
|
-
- jruby-9.2.4.0
|
13
|
+
- 2.4.6
|
14
|
+
- 2.5.5
|
15
|
+
- 2.6.3
|
16
|
+
- jruby-9.2.7.0
|
19
17
|
env:
|
20
18
|
global:
|
21
19
|
- JRUBY_OPTS='--dev -J-Xmx1024M'
|
22
20
|
- COVERAGE=true
|
21
|
+
- RSPEC_OPTS='--exclude-pattern spec/integration/pattern_matching_spec.rb'
|
23
22
|
matrix:
|
23
|
+
include:
|
24
|
+
- rvm: 2.7
|
25
|
+
env: "RSPEC_OPTS=''"
|
24
26
|
allow_failures:
|
25
27
|
- rvm: ruby-head
|
26
28
|
|
@@ -30,10 +32,10 @@ notifications:
|
|
30
32
|
- fg@flashgordon.ru
|
31
33
|
on_success: change
|
32
34
|
on_failure: always
|
33
|
-
on_start: false
|
35
|
+
on_start: false # default: false
|
34
36
|
webhooks:
|
35
37
|
urls:
|
36
38
|
- https://webhooks.gitter.im/e/19098b4253a72c9796db
|
37
|
-
on_success: change
|
38
|
-
on_failure: always
|
39
|
-
on_start: false
|
39
|
+
on_success: change # options: [always|never|change] default: always
|
40
|
+
on_failure: always # options: [always|never|change] default: always
|
41
|
+
on_start: false # default: false
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,88 @@
|
|
1
|
+
# v1.3.0 2019-08-03
|
2
|
+
|
3
|
+
## BREAKING CHANGES
|
4
|
+
|
5
|
+
* Support for Ruby 2.3 was dropped.
|
6
|
+
|
7
|
+
## Added
|
8
|
+
|
9
|
+
* `Result#either` (waiting-for-dev)
|
10
|
+
```ruby
|
11
|
+
Success(1).either(-> x { x + 1 }, -> x { x + 2 }) # => 2
|
12
|
+
Failure(1).either(-> x { x + 1 }, -> x { x + 2 }) # => 3
|
13
|
+
```
|
14
|
+
* `Maybe#to_result` (SpyMachine + flash-gordon)
|
15
|
+
```ruby
|
16
|
+
Some(3).to_result(:no_value) # => Success(3)
|
17
|
+
None().to_result { :no_value } # => Failure(:no_value)
|
18
|
+
None().to_result # => Failure()
|
19
|
+
```
|
20
|
+
* Do notation can be used with `extend`. This simplifies usage in class methods and in other "complicated" cases (gogiel + flash-gordon)
|
21
|
+
|
22
|
+
```ruby
|
23
|
+
class CreateUser
|
24
|
+
extend Dry::Monads::Do::Mixin
|
25
|
+
extend Dry::Monads[:result]
|
26
|
+
|
27
|
+
def self.run(params)
|
28
|
+
self.call do
|
29
|
+
values = bind Validator.validate(params)
|
30
|
+
user = bind UserRepository.create(values)
|
31
|
+
|
32
|
+
Success(user)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
```
|
37
|
+
|
38
|
+
Or you can bind values directly:
|
39
|
+
```ruby
|
40
|
+
ma = Dry::Monads.Success(1)
|
41
|
+
mb = Dry::Monads.Success(2)
|
42
|
+
|
43
|
+
Dry::Monads::Do.() do
|
44
|
+
a = Dry::Monads::Do.bind(ma)
|
45
|
+
b = Dry::Monads::Do.bind(mb)
|
46
|
+
|
47
|
+
Dry::Monads.Success(a + b)
|
48
|
+
end
|
49
|
+
```
|
50
|
+
* `{Some,Success,Failure}#[]` shortcuts for building arrays wrapped within monadic value (flash-gordon)
|
51
|
+
```ruby
|
52
|
+
Success[1, 2] # => Success([1, 2])
|
53
|
+
```
|
54
|
+
* `List.unfold` yields a block returning `Maybe<Any>`. If the block returns `Some(a)` `a` is appended to the output list. Returning `None` halts the unfloding (flash-gordon)
|
55
|
+
```ruby
|
56
|
+
List.unfold(0) do |x|
|
57
|
+
if x > 5
|
58
|
+
None()
|
59
|
+
else
|
60
|
+
Some[x + 1, 2**x]
|
61
|
+
end
|
62
|
+
end # => List[1, 2, 3, 4, 5]
|
63
|
+
```
|
64
|
+
|
65
|
+
* Experimental support for pattern matching! :tada: (flash-gordon)
|
66
|
+
```ruby
|
67
|
+
case value
|
68
|
+
in Failure(_) then :failure
|
69
|
+
in Success(10) then :ten
|
70
|
+
in Success(100..500 => code) then code
|
71
|
+
in Success() then :empty
|
72
|
+
in Success(:code, x) then x
|
73
|
+
in Success[:status, x] then x
|
74
|
+
in Success({ status: x }) then x
|
75
|
+
in Success({ code: 200..300 => x }) then x
|
76
|
+
end
|
77
|
+
```
|
78
|
+
Read more about pattern matching in Ruby:
|
79
|
+
- https://medium.com/@baweaver/ruby-2-7-pattern-matching-destructuring-on-point-90f56aaf7b4e
|
80
|
+
- https://bugs.ruby-lang.org/issues/14912
|
81
|
+
|
82
|
+
Keep in mind this feature is experimental and can be changed by 2.7 release. But it rocks already!
|
83
|
+
|
84
|
+
[Compare v1.2.0...v1.3.0](https://github.com/dry-rb/dry-monads/compare/v1.2.0...v1.3.0)
|
85
|
+
|
1
86
|
# v1.2.0 2019-01-12
|
2
87
|
|
3
88
|
## BREAKING CHANGES
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,17 +1,17 @@
|
|
1
1
|
[gitter]: https://gitter.im/dry-rb/chat
|
2
2
|
[gem]: https://rubygems.org/gems/dry-monads
|
3
|
-
[travis]: https://travis-ci.
|
3
|
+
[travis]: https://travis-ci.com/dry-rb/dry-monads
|
4
4
|
[code_climate]: https://codeclimate.com/github/dry-rb/dry-monads
|
5
5
|
[inch]: http://inch-ci.org/github/dry-rb/dry-monads
|
6
|
+
[chat]: https://dry-rb.zulipchat.com
|
6
7
|
|
7
|
-
# dry-monads [][chat]
|
8
9
|
|
9
10
|
[][gem]
|
10
11
|
[][travis]
|
11
12
|
[][code_climate]
|
12
13
|
[][code_climate]
|
13
14
|
[][inch]
|
14
|
-

|
15
15
|
|
16
16
|
Monads for Ruby.
|
17
17
|
|
@@ -37,7 +37,7 @@ $ gem install dry-monads
|
|
37
37
|
|
38
38
|
## Links
|
39
39
|
|
40
|
-
|
40
|
+
- [Documentation](http://dry-rb.org/gems/dry-monads)
|
41
41
|
|
42
42
|
## Development
|
43
43
|
|
data/dry-monads.gemspec
CHANGED
@@ -25,7 +25,7 @@ Gem::Specification.new do |spec|
|
|
25
25
|
spec.bindir = 'exe'
|
26
26
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
27
27
|
spec.require_paths = ['lib']
|
28
|
-
spec.required_ruby_version = ">= 2.
|
28
|
+
spec.required_ruby_version = ">= 2.4.0"
|
29
29
|
spec.add_dependency 'dry-equalizer'
|
30
30
|
spec.add_dependency 'dry-core', '~> 0.4', '>= 0.4.4'
|
31
31
|
spec.add_dependency 'concurrent-ruby', '~> 1.0'
|
data/lib/dry/monads.rb
CHANGED
@@ -5,6 +5,7 @@ module Dry
|
|
5
5
|
#
|
6
6
|
# @api public
|
7
7
|
module Monads
|
8
|
+
# @private
|
8
9
|
def self.included(base)
|
9
10
|
if all_loaded?
|
10
11
|
base.include(*constructors)
|
@@ -47,7 +48,7 @@ module Dry
|
|
47
48
|
@mixins.fetch_or_store(monads.hash) do
|
48
49
|
monads.each { |m| load_monad(m) }
|
49
50
|
mixins = monads.map { |m| registry.fetch(m) }
|
50
|
-
Module.new { include(*mixins) }.freeze
|
51
|
+
::Module.new { include(*mixins) }.freeze
|
51
52
|
end
|
52
53
|
end
|
53
54
|
end
|
data/lib/dry/monads/do.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'dry/monads/list'
|
2
|
+
require 'dry/monads/do/mixin'
|
2
3
|
|
3
4
|
module Dry
|
4
5
|
module Monads
|
@@ -6,9 +7,11 @@ module Dry
|
|
6
7
|
#
|
7
8
|
# @see Do.for
|
8
9
|
module Do
|
9
|
-
|
10
|
+
extend Mixin
|
11
|
+
|
12
|
+
# @api private
|
10
13
|
class Halt < StandardError
|
11
|
-
# @private
|
14
|
+
# @api private
|
12
15
|
attr_reader :result
|
13
16
|
|
14
17
|
def initialize(result)
|
@@ -18,126 +21,117 @@ module Dry
|
|
18
21
|
end
|
19
22
|
end
|
20
23
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
24
|
+
class << self
|
25
|
+
# Generates a module that passes a block to methods
|
26
|
+
# that either unwraps a single-valued monadic value or halts
|
27
|
+
# the execution.
|
28
|
+
#
|
29
|
+
# @example A complete example
|
30
|
+
#
|
31
|
+
# class CreateUser
|
32
|
+
# include Dry::Monads::Result::Mixin
|
33
|
+
# include Dry::Monads::Try::Mixin
|
34
|
+
# include Dry::Monads::Do.for(:call)
|
35
|
+
#
|
36
|
+
# attr_reader :user_repo
|
37
|
+
#
|
38
|
+
# def initialize(:user_repo)
|
39
|
+
# @user_repo = user_repo
|
40
|
+
# end
|
41
|
+
#
|
42
|
+
# def call(params)
|
43
|
+
# json = yield parse_json(params)
|
44
|
+
# hash = yield validate(json)
|
45
|
+
#
|
46
|
+
# user_repo.transaction do
|
47
|
+
# user = yield create_user(hash[:user])
|
48
|
+
# yield create_profile(user, hash[:profile])
|
49
|
+
# end
|
50
|
+
#
|
51
|
+
# Success(user)
|
52
|
+
# end
|
53
|
+
#
|
54
|
+
# private
|
55
|
+
#
|
56
|
+
# def parse_json(params)
|
57
|
+
# Try(JSON::ParserError) {
|
58
|
+
# JSON.parse(params)
|
59
|
+
# }.to_result
|
60
|
+
# end
|
61
|
+
#
|
62
|
+
# def validate(json)
|
63
|
+
# UserSchema.(json).to_monad
|
64
|
+
# end
|
65
|
+
#
|
66
|
+
# def create_user(user_data)
|
67
|
+
# Try(Sequel::Error) {
|
68
|
+
# user_repo.create(user_data)
|
69
|
+
# }.to_result
|
70
|
+
# end
|
71
|
+
#
|
72
|
+
# def create_profile(user, profile_data)
|
73
|
+
# Try(Sequel::Error) {
|
74
|
+
# user_repo.create_profile(user, profile_data)
|
75
|
+
# }.to_result
|
76
|
+
# end
|
77
|
+
# end
|
78
|
+
#
|
79
|
+
# @param [Array<Symbol>] methods
|
80
|
+
# @return [Module]
|
81
|
+
def for(*methods)
|
82
|
+
mod = ::Module.new do
|
83
|
+
methods.each { |method_name| Do.wrap_method(self, method_name) }
|
84
|
+
end
|
81
85
|
|
82
|
-
|
83
|
-
|
84
|
-
|
86
|
+
::Module.new do
|
87
|
+
singleton_class.send(:define_method, :included) do |base|
|
88
|
+
base.prepend(mod)
|
89
|
+
end
|
85
90
|
end
|
86
91
|
end
|
87
|
-
end
|
88
92
|
|
89
|
-
|
90
|
-
|
91
|
-
|
93
|
+
# @api private
|
94
|
+
def included(base)
|
95
|
+
super
|
92
96
|
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
protected
|
97
|
+
# Actually mixes in Do::All
|
98
|
+
require 'dry/monads/do/all'
|
99
|
+
base.include All
|
100
|
+
end
|
99
101
|
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
102
|
+
# @api private
|
103
|
+
def wrap_method(target, method_name)
|
104
|
+
target.module_eval(<<-RUBY, __FILE__, __LINE__ + 1)
|
105
|
+
def #{method_name}(*)
|
106
|
+
if block_given?
|
107
|
+
super
|
108
|
+
else
|
109
|
+
Do.() { super { |*ms| Do.bind(ms) } }
|
110
|
+
end
|
111
|
+
end
|
112
|
+
RUBY
|
113
|
+
end
|
104
114
|
|
105
|
-
|
106
|
-
|
107
|
-
|
115
|
+
# @api private
|
116
|
+
def coerce_to_monad(monads)
|
117
|
+
return monads if monads.size != 1
|
108
118
|
|
109
|
-
|
119
|
+
first = monads[0]
|
110
120
|
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
121
|
+
case first
|
122
|
+
when ::Array
|
123
|
+
[List.coerce(first).traverse]
|
124
|
+
when List
|
125
|
+
[first.traverse]
|
126
|
+
else
|
127
|
+
monads
|
128
|
+
end
|
118
129
|
end
|
119
|
-
end
|
120
130
|
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
if block_given?
|
126
|
-
super
|
127
|
-
else
|
128
|
-
super do |*monads|
|
129
|
-
monads = Do.coerce_to_monad(monads)
|
130
|
-
unwrapped = monads.map { |result|
|
131
|
-
monad = result.to_monad
|
132
|
-
monad.or { Do.halt(monad) }.value!
|
133
|
-
}
|
134
|
-
monads.size == 1 ? unwrapped[0] : unwrapped
|
135
|
-
end
|
136
|
-
end
|
137
|
-
rescue Halt => e
|
138
|
-
e.result
|
139
|
-
end
|
140
|
-
RUBY
|
131
|
+
# @api private
|
132
|
+
def halt(result)
|
133
|
+
raise Halt.new(result)
|
134
|
+
end
|
141
135
|
end
|
142
136
|
end
|
143
137
|
end
|
data/lib/dry/monads/do/all.rb
CHANGED
@@ -95,15 +95,39 @@ module Dry
|
|
95
95
|
end
|
96
96
|
end
|
97
97
|
|
98
|
-
# @private
|
98
|
+
# @api private
|
99
99
|
def self.included(base)
|
100
100
|
super
|
101
101
|
|
102
|
-
wrappers = Hash.new { |h, k| h[k] = Module.new }
|
102
|
+
wrappers = ::Hash.new { |h, k| h[k] = ::Module.new }
|
103
103
|
tracker = MethodTracker.new(wrappers)
|
104
104
|
base.extend(tracker)
|
105
105
|
base.instance_methods(false).each { |m| tracker.wrap_method(base, m) }
|
106
|
+
|
107
|
+
base.extend(InstanceMixin) unless base.is_a?(::Class)
|
106
108
|
end
|
109
|
+
|
110
|
+
# @api private
|
111
|
+
module InstanceMixin
|
112
|
+
# @api private
|
113
|
+
def extended(object)
|
114
|
+
super
|
115
|
+
|
116
|
+
wrapper = ::Module.new
|
117
|
+
object.singleton_class.prepend(wrapper)
|
118
|
+
object.define_singleton_method(:singleton_method_added) do |method|
|
119
|
+
super(method)
|
120
|
+
|
121
|
+
next if method.equal?(:singleton_method_added)
|
122
|
+
Do.wrap_method(wrapper, method)
|
123
|
+
end
|
124
|
+
object.singleton_class.instance_methods(false).each do |m|
|
125
|
+
Do.wrap_method(wrapper, m)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
extend InstanceMixin
|
107
131
|
end
|
108
132
|
end
|
109
133
|
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Dry
|
2
|
+
module Monads
|
3
|
+
module Do
|
4
|
+
# Do notation as a mixin.
|
5
|
+
# You can use it in any place in your code, see examples.
|
6
|
+
#
|
7
|
+
# @example class-level mixin
|
8
|
+
#
|
9
|
+
# class CreateUser
|
10
|
+
# extend Dry::Monads::Do::Mixin
|
11
|
+
# extend Dry::Monads[:result]
|
12
|
+
#
|
13
|
+
# def self.run(params)
|
14
|
+
# self.call do
|
15
|
+
# values = bind Validator.validate(params)
|
16
|
+
# user = bind UserRepository.create(values)
|
17
|
+
#
|
18
|
+
# Success(user)
|
19
|
+
# end
|
20
|
+
# end
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# @example using methods defined on Do
|
24
|
+
#
|
25
|
+
# create_user = proc do |params|
|
26
|
+
# Do.() do
|
27
|
+
# values = bind validate(params)
|
28
|
+
# user = bind user_repo.create(values)
|
29
|
+
#
|
30
|
+
# Success(user)
|
31
|
+
# end
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
# @api public
|
35
|
+
module Mixin
|
36
|
+
# @api public
|
37
|
+
def call
|
38
|
+
yield
|
39
|
+
rescue Halt => e
|
40
|
+
e.result
|
41
|
+
end
|
42
|
+
|
43
|
+
# @api public
|
44
|
+
def bind(monads)
|
45
|
+
monads = Do.coerce_to_monad(Array(monads))
|
46
|
+
unwrapped = monads.map { |result|
|
47
|
+
monad = result.to_monad
|
48
|
+
monad.or { Do.halt(monad) }.value!
|
49
|
+
}
|
50
|
+
monads.size == 1 ? unwrapped[0] : unwrapped
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
data/lib/dry/monads/errors.rb
CHANGED
@@ -13,5 +13,15 @@ module Dry
|
|
13
13
|
super("Cannot create Failure from #{ failure.inspect }, it doesn't meet the constraints")
|
14
14
|
end
|
15
15
|
end
|
16
|
+
|
17
|
+
# Improper use of None
|
18
|
+
class ConstructorNotAppliedError < NoMethodError
|
19
|
+
def initialize(method_name, constructor_name)
|
20
|
+
super(
|
21
|
+
"For calling .#{method_name} on #{constructor_name}() build a value "\
|
22
|
+
"by appending parens: #{constructor_name}()"
|
23
|
+
)
|
24
|
+
end
|
25
|
+
end
|
16
26
|
end
|
17
27
|
end
|
data/lib/dry/monads/list.rb
CHANGED
@@ -55,6 +55,30 @@ module Dry
|
|
55
55
|
new([value], type)
|
56
56
|
end
|
57
57
|
end
|
58
|
+
|
59
|
+
# Iteratively builds a new list from a block returning Maybe values
|
60
|
+
#
|
61
|
+
# @see https://hackage.haskell.org/package/base-4.12.0.0/docs/Data-List.html#g:9
|
62
|
+
#
|
63
|
+
# @param state [Object.new] Initial state
|
64
|
+
# @param type [#pure] Type of list element
|
65
|
+
# @return [List]
|
66
|
+
def unfold(state, type = nil)
|
67
|
+
xs = []
|
68
|
+
|
69
|
+
loop do
|
70
|
+
m = yield(state)
|
71
|
+
|
72
|
+
if m.some?
|
73
|
+
state, x = m.value!
|
74
|
+
xs << x
|
75
|
+
else
|
76
|
+
break
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
new(xs, type)
|
81
|
+
end
|
58
82
|
end
|
59
83
|
|
60
84
|
extend Dry::Core::Deprecations[:'dry-monads']
|
@@ -341,6 +365,20 @@ module Dry
|
|
341
365
|
end
|
342
366
|
end
|
343
367
|
|
368
|
+
# Pattern matching
|
369
|
+
#
|
370
|
+
# @example
|
371
|
+
# case List[1, 2, 3]
|
372
|
+
# in List[1, 2, x] then ...
|
373
|
+
# in List[Integer, _, _] then ...
|
374
|
+
# in List[0..2, _, _] then ...
|
375
|
+
# end
|
376
|
+
#
|
377
|
+
# @api private
|
378
|
+
def deconstruct
|
379
|
+
value
|
380
|
+
end
|
381
|
+
|
344
382
|
private
|
345
383
|
|
346
384
|
def coerce(other)
|
data/lib/dry/monads/maybe.rb
CHANGED
@@ -5,6 +5,7 @@ require 'dry/core/deprecations'
|
|
5
5
|
require 'dry/monads/right_biased'
|
6
6
|
require 'dry/monads/transformer'
|
7
7
|
require 'dry/monads/unit'
|
8
|
+
require 'dry/monads/undefined'
|
8
9
|
|
9
10
|
module Dry
|
10
11
|
module Monads
|
@@ -88,6 +89,20 @@ module Dry
|
|
88
89
|
include Dry::Equalizer(:value!)
|
89
90
|
include RightBiased::Right
|
90
91
|
|
92
|
+
# Shortcut for Some([...])
|
93
|
+
#
|
94
|
+
# @example
|
95
|
+
# include Dry::Monads[:maybe]
|
96
|
+
#
|
97
|
+
# def call
|
98
|
+
# Some[200, {}, ['ok']] # => Some([200, {}, ['ok']])
|
99
|
+
# end
|
100
|
+
#
|
101
|
+
# @api public
|
102
|
+
def self.[](*value)
|
103
|
+
new(value)
|
104
|
+
end
|
105
|
+
|
91
106
|
def initialize(value = Undefined)
|
92
107
|
raise ArgumentError, 'nil cannot be some' if value.nil?
|
93
108
|
@value = Undefined.default(value, Unit)
|
@@ -104,12 +119,17 @@ module Dry
|
|
104
119
|
# @return [Maybe::Some, Maybe::None] Wrapped result, i.e. nil will be mapped to None,
|
105
120
|
# other values will be wrapped with Some
|
106
121
|
def fmap(*args, &block)
|
107
|
-
|
122
|
+
Maybe.coerce(bind(*args, &block))
|
108
123
|
end
|
124
|
+
alias_method :maybe, :fmap
|
109
125
|
|
110
126
|
# @return [String]
|
111
127
|
def to_s
|
112
|
-
|
128
|
+
if Unit.equal?(@value)
|
129
|
+
'Some()'
|
130
|
+
else
|
131
|
+
"Some(#{@value.inspect})"
|
132
|
+
end
|
113
133
|
end
|
114
134
|
alias_method :inspect, :to_s
|
115
135
|
end
|
@@ -119,10 +139,21 @@ module Dry
|
|
119
139
|
# @api public
|
120
140
|
class None < Maybe
|
121
141
|
include RightBiased::Left
|
142
|
+
include Core::Constants
|
122
143
|
|
123
144
|
@instance = new.freeze
|
124
145
|
singleton_class.send(:attr_reader, :instance)
|
125
146
|
|
147
|
+
# @api private
|
148
|
+
def self.method_missing(m, *)
|
149
|
+
if (instance.methods(true) - methods(true)).include?(m)
|
150
|
+
raise ConstructorNotAppliedError.new(m, :None)
|
151
|
+
else
|
152
|
+
super
|
153
|
+
end
|
154
|
+
end
|
155
|
+
private_class_method :method_missing
|
156
|
+
|
126
157
|
# Line where the value was constructed
|
127
158
|
#
|
128
159
|
# @return [String]
|
@@ -180,6 +211,20 @@ module Dry
|
|
180
211
|
def hash
|
181
212
|
None.instance.object_id
|
182
213
|
end
|
214
|
+
|
215
|
+
# Pattern matching
|
216
|
+
#
|
217
|
+
# @example
|
218
|
+
# case Some(:foo)
|
219
|
+
# in Some(Integer) then ...
|
220
|
+
# in Some(:bar) then ...
|
221
|
+
# in None() then ...
|
222
|
+
# end
|
223
|
+
#
|
224
|
+
# @api private
|
225
|
+
def deconstruct
|
226
|
+
EMPTY_ARRAY
|
227
|
+
end
|
183
228
|
end
|
184
229
|
|
185
230
|
# A module that can be included for easier access to Maybe monads.
|
@@ -222,6 +267,48 @@ module Dry
|
|
222
267
|
|
223
268
|
include Constructors
|
224
269
|
end
|
270
|
+
|
271
|
+
# Utilities for working with hashes storing Maybe values
|
272
|
+
module Hash
|
273
|
+
# Traverses a hash with maybe values. If any value is None then None is returned
|
274
|
+
#
|
275
|
+
# @example
|
276
|
+
# Maybe::Hash.all(foo: Some(1), bar: Some(2)) # => Some(foo: 1, bar: 2)
|
277
|
+
# Maybe::Hash.all(foo: Some(1), bar: None()) # => None()
|
278
|
+
# Maybe::Hash.all(foo: None(), bar: Some(2)) # => None()
|
279
|
+
#
|
280
|
+
# @param hash [::Hash<Object,Maybe>]
|
281
|
+
# @return [Maybe<::Hash>]
|
282
|
+
#
|
283
|
+
def self.all(hash, trace = RightBiased::Left.trace_caller)
|
284
|
+
result = hash.each_with_object({}) do |(key, value), output|
|
285
|
+
if value.some?
|
286
|
+
output[key] = value.value!
|
287
|
+
else
|
288
|
+
return None.new(trace)
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
Some.new(result)
|
293
|
+
end
|
294
|
+
|
295
|
+
# Traverses a hash with maybe values. Some values are unwrapped, keys with
|
296
|
+
# None values are removed
|
297
|
+
#
|
298
|
+
# @example
|
299
|
+
# Maybe::Hash.filter(foo: Some(1), bar: Some(2)) # => Some(foo: 1, bar: 2)
|
300
|
+
# Maybe::Hash.filter(foo: Some(1), bar: None()) # => None()
|
301
|
+
# Maybe::Hash.filter(foo: None(), bar: Some(2)) # => None()
|
302
|
+
#
|
303
|
+
# @param hash [::Hash<Object,Maybe>]
|
304
|
+
# @return [::Hash]
|
305
|
+
#
|
306
|
+
def self.filter(hash)
|
307
|
+
hash.each_with_object({}) do |(key, value), output|
|
308
|
+
output[key] = value.value! if value.some?
|
309
|
+
end
|
310
|
+
end
|
311
|
+
end
|
225
312
|
end
|
226
313
|
|
227
314
|
extend Maybe::Mixin::Constructors
|
data/lib/dry/monads/result.rb
CHANGED
@@ -66,6 +66,20 @@ module Dry
|
|
66
66
|
include RightBiased::Right
|
67
67
|
include Dry::Equalizer(:value!)
|
68
68
|
|
69
|
+
# Shortcut for Success([...])
|
70
|
+
#
|
71
|
+
# @example
|
72
|
+
# include Dry::Monads[:result]
|
73
|
+
#
|
74
|
+
# def call
|
75
|
+
# Success[200, {}, ['ok']] # => Success([200, {}, ['ok']])
|
76
|
+
# end
|
77
|
+
#
|
78
|
+
# @api public
|
79
|
+
def self.[](*value)
|
80
|
+
new(value)
|
81
|
+
end
|
82
|
+
|
69
83
|
alias_method :success, :value!
|
70
84
|
|
71
85
|
# @param value [Object] a value of a successful operation
|
@@ -103,9 +117,25 @@ module Dry
|
|
103
117
|
Success.new(bind(*args, &block))
|
104
118
|
end
|
105
119
|
|
120
|
+
# Returns result of applying first function to the internal value.
|
121
|
+
#
|
122
|
+
# @example
|
123
|
+
# Dry::Monads.Success(1).either(-> x { x + 1 }, -> x { x + 2 }) # => 2
|
124
|
+
#
|
125
|
+
# @param f [#call] Function to apply
|
126
|
+
# @param _ [#call] Ignored
|
127
|
+
# @return [Any] Return value of `f`
|
128
|
+
def either(f, _)
|
129
|
+
f.(success)
|
130
|
+
end
|
131
|
+
|
106
132
|
# @return [String]
|
107
133
|
def to_s
|
108
|
-
|
134
|
+
if Unit.equal?(@value)
|
135
|
+
'Success()'
|
136
|
+
else
|
137
|
+
"Success(#{@value.inspect})"
|
138
|
+
end
|
109
139
|
end
|
110
140
|
alias_method :inspect, :to_s
|
111
141
|
|
@@ -126,6 +156,20 @@ module Dry
|
|
126
156
|
|
127
157
|
singleton_class.send(:alias_method, :call, :new)
|
128
158
|
|
159
|
+
# Shortcut for Failure([...])
|
160
|
+
#
|
161
|
+
# @example
|
162
|
+
# include Dry::Monads[:result]
|
163
|
+
#
|
164
|
+
# def call
|
165
|
+
# Failure[:error, :not_found] # => Failure([:error, :not_found])
|
166
|
+
# end
|
167
|
+
#
|
168
|
+
# @api public
|
169
|
+
def self.[](*value)
|
170
|
+
new(value, RightBiased::Left.trace_caller)
|
171
|
+
end
|
172
|
+
|
129
173
|
# Returns a constructor proc
|
130
174
|
#
|
131
175
|
# @return [Proc]
|
@@ -200,7 +244,11 @@ module Dry
|
|
200
244
|
|
201
245
|
# @return [String]
|
202
246
|
def to_s
|
203
|
-
|
247
|
+
if Unit.equal?(@value)
|
248
|
+
'Failure()'
|
249
|
+
else
|
250
|
+
"Failure(#{@value.inspect})"
|
251
|
+
end
|
204
252
|
end
|
205
253
|
alias_method :inspect, :to_s
|
206
254
|
|
@@ -225,6 +273,18 @@ module Dry
|
|
225
273
|
def ===(other)
|
226
274
|
Failure === other && failure === other.failure
|
227
275
|
end
|
276
|
+
|
277
|
+
# Returns result of applying second function to the internal value.
|
278
|
+
#
|
279
|
+
# @example
|
280
|
+
# Dry::Monads.Failure(1).either(-> x { x + 1 }, -> x { x + 2 }) # => 3
|
281
|
+
#
|
282
|
+
# @param _ [#call] Ignored
|
283
|
+
# @param g [#call] Function to call
|
284
|
+
# @return [Any] Return value of `g`
|
285
|
+
def either(_, g)
|
286
|
+
g.(failure)
|
287
|
+
end
|
228
288
|
end
|
229
289
|
|
230
290
|
# A module that can be included for easier access to Result monads.
|
@@ -319,6 +379,34 @@ module Dry
|
|
319
379
|
Result::Fixed[error, **options]
|
320
380
|
end
|
321
381
|
|
382
|
+
class Maybe
|
383
|
+
class Some < Maybe
|
384
|
+
# Converts to Sucess(value!)
|
385
|
+
#
|
386
|
+
# @param fail [#call] Fallback value
|
387
|
+
# @param block [Proc] Fallback block
|
388
|
+
# @return [Success<Any>]
|
389
|
+
def to_result(fail = Unit, &block)
|
390
|
+
Result::Success.new(@value)
|
391
|
+
end
|
392
|
+
end
|
393
|
+
|
394
|
+
class None < Maybe
|
395
|
+
# Converts to Failure(fallback_value)
|
396
|
+
#
|
397
|
+
# @param fail [#call] Fallback value
|
398
|
+
# @param block [Proc] Fallback block
|
399
|
+
# @return [Failure<Any>]
|
400
|
+
def to_result(fail = Unit, &block)
|
401
|
+
if block_given?
|
402
|
+
Result::Failure.new(yield)
|
403
|
+
else
|
404
|
+
Result::Failure.new(fail)
|
405
|
+
end
|
406
|
+
end
|
407
|
+
end
|
408
|
+
end
|
409
|
+
|
322
410
|
class Task
|
323
411
|
# Converts to Result. Blocks the current thread if required.
|
324
412
|
#
|
@@ -186,6 +186,25 @@ module Dry
|
|
186
186
|
end
|
187
187
|
end
|
188
188
|
|
189
|
+
# Pattern matching
|
190
|
+
#
|
191
|
+
# @example
|
192
|
+
# case Success(x)
|
193
|
+
# in Success(Integer) then ...
|
194
|
+
# in Success(2..100) then ...
|
195
|
+
# in Success(2..200 => code) then ...
|
196
|
+
# end
|
197
|
+
# @api private
|
198
|
+
def deconstruct
|
199
|
+
if Unit.equal?(@value)
|
200
|
+
[]
|
201
|
+
elsif @value.is_a?(::Array)
|
202
|
+
@value
|
203
|
+
else
|
204
|
+
[@value]
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
189
208
|
private
|
190
209
|
|
191
210
|
# @api private
|
@@ -303,6 +322,27 @@ module Dry
|
|
303
322
|
def and(_)
|
304
323
|
self
|
305
324
|
end
|
325
|
+
|
326
|
+
# Pattern matching
|
327
|
+
#
|
328
|
+
# @example
|
329
|
+
# case Success(x)
|
330
|
+
# in Success(Integer) then ...
|
331
|
+
# in Success(2..100) then ...
|
332
|
+
# in Success(2..200 => code) then ...
|
333
|
+
# in Failure(_) then ...
|
334
|
+
# end
|
335
|
+
#
|
336
|
+
# @api private
|
337
|
+
def deconstruct
|
338
|
+
if Unit.equal?(@value)
|
339
|
+
[]
|
340
|
+
elsif @value.is_a?(::Array)
|
341
|
+
@value
|
342
|
+
else
|
343
|
+
[@value]
|
344
|
+
end
|
345
|
+
end
|
306
346
|
end
|
307
347
|
end
|
308
348
|
end
|
data/lib/dry/monads/task.rb
CHANGED
@@ -128,14 +128,18 @@ module Dry
|
|
128
128
|
def to_s
|
129
129
|
state = case promise.state
|
130
130
|
when :fulfilled
|
131
|
-
|
131
|
+
if Unit.equal?(value!)
|
132
|
+
'value=()'
|
133
|
+
else
|
134
|
+
"value=#{value!.inspect}"
|
135
|
+
end
|
132
136
|
when :rejected
|
133
137
|
"error=#{ promise.reason.inspect }"
|
134
138
|
else
|
135
139
|
'?'
|
136
140
|
end
|
137
141
|
|
138
|
-
"Task(#{
|
142
|
+
"Task(#{state})"
|
139
143
|
end
|
140
144
|
alias_method :inspect, :to_s
|
141
145
|
|
data/lib/dry/monads/try.rb
CHANGED
@@ -20,7 +20,7 @@ module Dry
|
|
20
20
|
attr_reader :exception
|
21
21
|
|
22
22
|
class << self
|
23
|
-
extend
|
23
|
+
extend Core::Deprecations[:'dry-monads']
|
24
24
|
|
25
25
|
# Invokes a callable and if successful stores the result in the
|
26
26
|
# {Try::Value} type, but if one of the specified exceptions was raised it stores
|
@@ -101,7 +101,7 @@ module Dry
|
|
101
101
|
include Dry::Equalizer(:value!, :catchable)
|
102
102
|
include RightBiased::Right
|
103
103
|
|
104
|
-
# @
|
104
|
+
# @return [Array<Exception>] List of exceptions to rescue
|
105
105
|
attr_reader :catchable
|
106
106
|
|
107
107
|
# @param exceptions [Array<Exception>] list of exceptions to be rescued
|
@@ -156,7 +156,11 @@ module Dry
|
|
156
156
|
|
157
157
|
# @return [String]
|
158
158
|
def to_s
|
159
|
-
|
159
|
+
if Unit.equal?(@value)
|
160
|
+
'Try::Value()'
|
161
|
+
else
|
162
|
+
"Try::Value(#{@value.inspect})"
|
163
|
+
end
|
160
164
|
end
|
161
165
|
alias_method :inspect, :to_s
|
162
166
|
end
|
@@ -225,6 +229,7 @@ module Dry
|
|
225
229
|
# @see Dry::Monads::Try
|
226
230
|
Try = Try
|
227
231
|
|
232
|
+
# @private
|
228
233
|
module Constructors
|
229
234
|
# A convenience wrapper for {Monads::Try.run}.
|
230
235
|
# If no exceptions are provided it falls back to StandardError.
|
@@ -234,7 +239,7 @@ module Dry
|
|
234
239
|
# @param exceptions [Array<Exception>]
|
235
240
|
# @return [Try]
|
236
241
|
def Try(*exceptions, &f)
|
237
|
-
catchable = exceptions.empty? ?
|
242
|
+
catchable = exceptions.empty? ? DEFAULT_EXCEPTIONS : exceptions.flatten
|
238
243
|
Try.run(catchable, f)
|
239
244
|
end
|
240
245
|
end
|
data/lib/dry/monads/validated.rb
CHANGED
data/lib/dry/monads/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dry-monads
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nikita Shilnikov
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-
|
11
|
+
date: 2019-08-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: dry-equalizer
|
@@ -142,6 +142,7 @@ files:
|
|
142
142
|
- lib/dry/monads/curry.rb
|
143
143
|
- lib/dry/monads/do.rb
|
144
144
|
- lib/dry/monads/do/all.rb
|
145
|
+
- lib/dry/monads/do/mixin.rb
|
145
146
|
- lib/dry/monads/either.rb
|
146
147
|
- lib/dry/monads/errors.rb
|
147
148
|
- lib/dry/monads/lazy.rb
|
@@ -174,14 +175,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
174
175
|
requirements:
|
175
176
|
- - ">="
|
176
177
|
- !ruby/object:Gem::Version
|
177
|
-
version: 2.
|
178
|
+
version: 2.4.0
|
178
179
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
179
180
|
requirements:
|
180
181
|
- - ">="
|
181
182
|
- !ruby/object:Gem::Version
|
182
183
|
version: '0'
|
183
184
|
requirements: []
|
184
|
-
rubygems_version: 3.0.
|
185
|
+
rubygems_version: 3.0.3
|
185
186
|
signing_key:
|
186
187
|
specification_version: 4
|
187
188
|
summary: Common monads for Ruby.
|