dry-monads 1.2.0 → 1.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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 [![Join the
|
8
|
+
# dry-monads [![Join the chat at https://dry-rb.zulipchat.com](https://img.shields.io/badge/dry--rb-join%20chat-%23346b7a.svg)][chat]
|
8
9
|
|
9
10
|
[![Gem Version](https://img.shields.io/gem/v/dry-monads.svg)][gem]
|
10
11
|
[![Build Status](https://img.shields.io/travis/dry-rb/dry-monads.svg)][travis]
|
11
12
|
[![Code Climate](https://api.codeclimate.com/v1/badges/b0ea4d8023d53b7f0f50/maintainability)][code_climate]
|
12
13
|
[![Test Coverage](https://api.codeclimate.com/v1/badges/b0ea4d8023d53b7f0f50/test_coverage)][code_climate]
|
13
14
|
[![API Documentation Coverage](http://inch-ci.org/github/dry-rb/dry-monads.svg)][inch]
|
14
|
-
![No monkey-patches](https://img.shields.io/badge/monkey--patches-0-brightgreen.svg)
|
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.
|