dry-monads 1.0.0.beta1 → 1.0.0.beta2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +30 -19
- data/bin/console +1 -1
- data/lib/dry/monads.rb +5 -78
- data/lib/dry/monads/all.rb +26 -0
- data/lib/dry/monads/conversion_stubs.rb +31 -0
- data/lib/dry/monads/do.rb +39 -26
- data/lib/dry/monads/do/all.rb +98 -0
- data/lib/dry/monads/lazy.rb +5 -0
- data/lib/dry/monads/list.rb +1 -2
- data/lib/dry/monads/maybe.rb +73 -0
- data/lib/dry/monads/result.rb +94 -27
- data/lib/dry/monads/right_biased.rb +26 -5
- data/lib/dry/monads/task.rb +18 -23
- data/lib/dry/monads/try.rb +5 -22
- data/lib/dry/monads/undefined.rb +8 -0
- data/lib/dry/monads/unit.rb +15 -0
- data/lib/dry/monads/validated.rb +40 -26
- data/lib/dry/monads/version.rb +1 -1
- metadata +7 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9b72d0fd34804b1fb9f751b61ffe8799386c3836a90625266948469439f576ad
|
4
|
+
data.tar.gz: 3303c72e17d0e905a8ac036c9ebd316fdfcbf6414ab3473d5c18a70ddf5fba01
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d7ec39938788dcc849f49125404790685785e76a881624df0ab0dd6ff7a9d25d1af2938dfa1af9854cc6b007417c0079a1f6d378bd155704f980cce02a6c279a
|
7
|
+
data.tar.gz: 1b9c4604241164988bc65e5da83b7d33445fda6c7de4de8f93d04d4cf2ae3d0e3592d367961469fe6858f7f58c7b83a89c5a7de01cf99a8108b3294370804b07
|
data/CHANGELOG.md
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
## Added
|
4
4
|
|
5
|
-
* `do`-like notation (the idea comes from Haskell of course). This is the biggest and most important addition
|
5
|
+
* `do`-like notation (the idea comes from Haskell of course). This is the biggest and most important addition to the release which greatly increases the ergonomics of using monads in Ruby. Basically, almost everything it does is passing a block to a given method. You call `yield` on monads to extract the values. If any operation fails i.e. no value can be extracted, the whole computation is halted and the failing step becomes a result. With `Do` you don't need to chain monadic values with `fmap/bind` and block, everything can be done on a single level of indentation. Here is a more or less real-life example:
|
6
6
|
|
7
7
|
```ruby
|
8
8
|
class CreateUser
|
@@ -42,7 +42,7 @@
|
|
42
42
|
def create_user(user_data)
|
43
43
|
Try[Sequel::Error] { user_repo.create(user_data) }.to_result
|
44
44
|
end
|
45
|
-
|
45
|
+
|
46
46
|
def create_profile(user, profile_data)
|
47
47
|
Try[Sequel::Error] {
|
48
48
|
user_repo.create_profile(user, profile_data)
|
@@ -53,7 +53,7 @@
|
|
53
53
|
|
54
54
|
In the code above any `yield` can potentially fail and return the failure reason as a result. In other words, `yield None` acts as `return None`. Internally, `Do` uses exceptions, not `return`, this is somewhat slower but allows to detect failed operations in DB-transactions and roll back the changes which far more useful than an unjustifiable speed boost (flash-gordon)
|
55
55
|
|
56
|
-
* The `Task` monad based on `Promise` from the [`concurrent-ruby` gem](https://github.com/ruby-concurrency/concurrent-ruby/). `Task` represents an
|
56
|
+
* The `Task` monad based on `Promise` from the [`concurrent-ruby` gem](https://github.com/ruby-concurrency/concurrent-ruby/). `Task` represents an asynchronous computation which _can be_ (doesn't have to!) run on a separated thread. `Promise` already offers a good API and implemented in a safe manner so `dry-monads` just adds a monad-compatible interface for it. Out of the box, `concurrent-ruby` has three types of executors for running blocks: `:io`, `:fast`, `:immediate`, check out [the docs](http://ruby-concurrency.github.io/concurrent-ruby/root/Concurrent.html#executor-class_method) for details. You can provide your own executor if needed (flash-gordon)
|
57
57
|
|
58
58
|
```ruby
|
59
59
|
include Dry::Monads::Task::Mixin
|
@@ -61,7 +61,7 @@
|
|
61
61
|
def call
|
62
62
|
name = Task { get_name_via_http } # runs a request in the background
|
63
63
|
email = Task { get_email_via_http } # runs another one request in the background
|
64
|
-
|
64
|
+
|
65
65
|
# to_result forces both computations/requests to complete by pausing current thread
|
66
66
|
# returns `Result::Success/Result::Failure`
|
67
67
|
name.bind { |n| email.fmap { |e| create(e, n) } }.to_result
|
@@ -87,15 +87,15 @@
|
|
87
87
|
list = List::Task[Task { get_name }, Task { get_email }]
|
88
88
|
list.traverse # => Task(List['John', 'john@doe.org'])
|
89
89
|
```
|
90
|
-
|
90
|
+
|
91
91
|
The code above runs two tasks in parallel and automatically combines their results with `traverse` (flash-gordon)
|
92
|
-
|
92
|
+
|
93
93
|
* `Try` got a new call syntax supported in Ruby 2.5+
|
94
94
|
|
95
95
|
```ruby
|
96
|
-
Try[ArgumentError, TypeError] { unsafe_operation }
|
96
|
+
Try[ArgumentError, TypeError] { unsafe_operation }
|
97
97
|
```
|
98
|
-
|
98
|
+
|
99
99
|
Prior to 2.5, it wasn't possible to pass a block to `[]`.
|
100
100
|
|
101
101
|
* The `Validated` “monad” that represents a result of a validation. Suppose, you want to collect all the errors and return them at once. You can't have it with `Result` because when you `traverse` a `List` of `Result`s it returns the first value and this is the correct behavior from the theoretical point of view. `Validated`, in fact, doesn't have a monad instance but provides a useful variant of applicative which concatenates the errors.
|
@@ -103,31 +103,41 @@
|
|
103
103
|
```ruby
|
104
104
|
include Dry::Monads
|
105
105
|
include Dry::Monads::Do.for(:call)
|
106
|
-
|
106
|
+
|
107
107
|
def call(input)
|
108
108
|
name, email = yield [
|
109
109
|
validate_name(input[:name]),
|
110
110
|
validate_email(input[:email])
|
111
111
|
]
|
112
|
-
|
112
|
+
|
113
113
|
Success(create(name, email))
|
114
114
|
end
|
115
|
-
|
116
|
-
# can return
|
115
|
+
|
116
|
+
# can return
|
117
117
|
# * Success(User(...))
|
118
118
|
# * Invalid(List[:invalid_name])
|
119
|
-
# * Invalid(List[:invalid_email])
|
120
|
-
# * Invalid(List[:invalid_name, :invalid_email])
|
119
|
+
# * Invalid(List[:invalid_email])
|
120
|
+
# * Invalid(List[:invalid_name, :invalid_email])
|
121
121
|
```
|
122
|
-
|
123
|
-
In the example above an array of `Validated` values is implicitly
|
124
|
-
|
125
|
-
* `Failure`, `None`, and `Invalid` values now store the line where they were created.
|
122
|
+
|
123
|
+
In the example above an array of `Validated` values is implicitly coerced to `List::Validated`. It's supported because it's useful but don't forget it's all about types so don't mix different types of monads in a single array, the consequences are unclear. You always can be explicit with `List::Validated[validate_name(...), ...]`, choose what you like (flash-gordon).
|
124
|
+
|
125
|
+
* `Failure`, `None`, and `Invalid` values now store the line where they were created. One of the biggest downsides of dealing with monadic code is lack of backtraces. If you have a long list of computations and one of them fails how do you know where did it actually happen? Say, you've got `None` and this tells you nothing about _what variable_ was assigned to `None`. It makes sense to use `Result` instead of `Maybe` and use distinct errors everywhere but it doesn't always look good and forces you to think more. TLDR; call `.trace` to get the line where a fail-case was constructed
|
126
126
|
|
127
127
|
```ruby
|
128
128
|
Failure(:invalid_name).trace # => app/operations/create_user.rb:43
|
129
129
|
```
|
130
|
-
|
130
|
+
|
131
|
+
* `Dry::Monads::Unit` which can be used as a replacement for `Success(nil)` and in similar situations when you have side effects yet doesn't return anything meningful as a result. There's also the `.discard` method for mapping any successful result (i.e. `Success(?)`, `Some(?)`, `Value(?)`, etc) to `Unit`.
|
132
|
+
|
133
|
+
```ruby
|
134
|
+
# we're making an HTTP request but "forget" any successful result,
|
135
|
+
# we only care if the task was complete without an error
|
136
|
+
Task { do_http_request }.discard
|
137
|
+
# ... wait for the task to finish ...
|
138
|
+
# => Task(valut=Unit)
|
139
|
+
```
|
140
|
+
|
131
141
|
## Deprecations
|
132
142
|
|
133
143
|
* `Either`, the former name of `Result`, is now deprecated
|
@@ -135,6 +145,7 @@
|
|
135
145
|
## BREAKING CHANGES
|
136
146
|
|
137
147
|
* `Either#value` and `Maybe#value` were both droped, use `value_or` or `value!` when you :100: sure it's safe
|
148
|
+
* `require 'dry/monads'` doesn't load all monads anymore, use `require 'dry/monads/all'` instead or cherry pick them with `require 'dry/monads/maybe'` etc (timriley)
|
138
149
|
|
139
150
|
[Compare v0.4.0...v1.0.0](https://github.com/dry-rb/dry-monads/compare/v0.4.0...master)
|
140
151
|
|
data/bin/console
CHANGED
data/lib/dry/monads.rb
CHANGED
@@ -1,87 +1,14 @@
|
|
1
|
-
require 'dry/core/constants'
|
2
|
-
require 'dry/monads/maybe'
|
3
|
-
require 'dry/monads/try'
|
4
|
-
require 'dry/monads/list'
|
5
|
-
require 'dry/monads/task'
|
6
|
-
require 'dry/monads/lazy'
|
7
|
-
require 'dry/monads/result'
|
8
|
-
require 'dry/monads/result/fixed'
|
9
|
-
require 'dry/monads/do'
|
10
|
-
require 'dry/monads/validated'
|
11
|
-
|
12
1
|
module Dry
|
13
2
|
# Common, idiomatic monads for Ruby
|
14
3
|
#
|
15
4
|
# @api public
|
16
5
|
module Monads
|
17
|
-
# @private
|
18
|
-
Undefined = Dry::Core::Constants::Undefined
|
19
|
-
|
20
|
-
# List of monad constructors
|
21
|
-
CONSTRUCTORS = [
|
22
|
-
Maybe::Mixin::Constructors,
|
23
|
-
Result::Mixin::Constructors,
|
24
|
-
Validated::Mixin::Constructors,
|
25
|
-
Try::Mixin::Constructors,
|
26
|
-
Task::Mixin::Constructors,
|
27
|
-
Lazy::Mixin::Constructors
|
28
|
-
].freeze
|
29
|
-
|
30
|
-
# @see Maybe::Some
|
31
|
-
Some = Maybe::Some
|
32
|
-
# @see Maybe::None
|
33
|
-
None = Maybe::None
|
34
|
-
# @see Result::Success
|
35
|
-
Success = Result::Success
|
36
|
-
# @see Result::Failure
|
37
|
-
Failure = Result::Failure
|
38
|
-
# @see Validated::Valid
|
39
|
-
Valid = Validated::Valid
|
40
|
-
# @see Validated::Invalid
|
41
|
-
Invalid = Validated::Invalid
|
42
|
-
|
43
|
-
extend(*CONSTRUCTORS)
|
44
|
-
|
45
|
-
# @private
|
46
6
|
def self.included(base)
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
# Creates a module that has two methods: `Success` and `Failure`.
|
53
|
-
# `Success` is identical to {Result::Mixin::Constructors#Success} and Failure
|
54
|
-
# rejects values that don't conform the value of the `error`
|
55
|
-
# parameter. This is essentially a Result type with the `Failure` part
|
56
|
-
# fixed.
|
57
|
-
#
|
58
|
-
# @example using dry-types
|
59
|
-
# module Types
|
60
|
-
# include Dry::Types.module
|
61
|
-
# end
|
62
|
-
#
|
63
|
-
# class Operation
|
64
|
-
# # :user_not_found and :account_not_found are the only
|
65
|
-
# # values allowed as failure results
|
66
|
-
# Error =
|
67
|
-
# Types.Value(:user_not_found) |
|
68
|
-
# Types.Value(:account_not_found)
|
69
|
-
#
|
70
|
-
# def find_account(id)
|
71
|
-
# account = acount_repo.find(id)
|
72
|
-
#
|
73
|
-
# account ? Success(account) : Failure(:account_not_found)
|
74
|
-
# end
|
75
|
-
#
|
76
|
-
# def find_user(id)
|
77
|
-
# # ...
|
78
|
-
# end
|
79
|
-
# end
|
80
|
-
#
|
81
|
-
# @param error [#===] the type of allowed failures
|
82
|
-
# @return [Module]
|
83
|
-
def self.Result(error, **options)
|
84
|
-
Result::Fixed[error, **options]
|
7
|
+
if const_defined?(:CONSTRUCTORS)
|
8
|
+
base.include(*CONSTRUCTORS)
|
9
|
+
else
|
10
|
+
raise "Load all monads first with require 'dry/monads/all'"
|
11
|
+
end
|
85
12
|
end
|
86
13
|
end
|
87
14
|
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'dry/monads'
|
2
|
+
require 'dry/monads/do'
|
3
|
+
require 'dry/monads/lazy'
|
4
|
+
require 'dry/monads/list'
|
5
|
+
require 'dry/monads/maybe'
|
6
|
+
require 'dry/monads/result'
|
7
|
+
require 'dry/monads/result/fixed'
|
8
|
+
require 'dry/monads/task'
|
9
|
+
require 'dry/monads/try'
|
10
|
+
require 'dry/monads/validated'
|
11
|
+
|
12
|
+
module Dry
|
13
|
+
module Monads
|
14
|
+
# List of monad constructors
|
15
|
+
CONSTRUCTORS = [
|
16
|
+
Lazy::Mixin::Constructors,
|
17
|
+
Maybe::Mixin::Constructors,
|
18
|
+
Result::Mixin::Constructors,
|
19
|
+
Task::Mixin::Constructors,
|
20
|
+
Try::Mixin::Constructors,
|
21
|
+
Validated::Mixin::Constructors,
|
22
|
+
].freeze
|
23
|
+
|
24
|
+
extend(*CONSTRUCTORS)
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Dry
|
2
|
+
module Monads
|
3
|
+
module ConversionStubs
|
4
|
+
def self.[](*method_names)
|
5
|
+
Module.new do
|
6
|
+
method_names.each do |name|
|
7
|
+
define_method(name) do |*|
|
8
|
+
Methods.public_send(name)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
module Methods
|
15
|
+
module_function
|
16
|
+
|
17
|
+
def to_maybe
|
18
|
+
raise "Load Maybe first with require 'dry/monads/maybe'"
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_result
|
22
|
+
raise "Load Result first with require 'dry/monads/result'"
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_validated
|
26
|
+
raise "Load Validated first with require 'dry/monads/validated'"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/lib/dry/monads/do.rb
CHANGED
@@ -74,45 +74,58 @@ module Dry
|
|
74
74
|
# @return [Module]
|
75
75
|
def self.for(*methods)
|
76
76
|
mod = Module.new do
|
77
|
-
methods.each
|
78
|
-
class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
|
79
|
-
def #{ method }(*)
|
80
|
-
super do |*ms|
|
81
|
-
ms = coerce_to_monad(ms)
|
82
|
-
unwrapped = ms.map { |m| m.to_monad.or { halt(m) }.value! }
|
83
|
-
ms.size == 1 ? unwrapped[0] : unwrapped
|
84
|
-
end
|
85
|
-
rescue Halt => e
|
86
|
-
e.result
|
87
|
-
end
|
88
|
-
RUBY
|
89
|
-
end
|
77
|
+
methods.each { |m| Do.wrap_method(self, m) }
|
90
78
|
end
|
91
79
|
|
92
80
|
Module.new do
|
93
81
|
singleton_class.send(:define_method, :included) do |base|
|
94
82
|
base.prepend(mod)
|
95
83
|
end
|
84
|
+
end
|
85
|
+
end
|
96
86
|
|
97
|
-
|
98
|
-
raise Halt.new(result)
|
99
|
-
end
|
87
|
+
protected
|
100
88
|
|
101
|
-
|
102
|
-
|
103
|
-
|
89
|
+
# @private
|
90
|
+
def self.halt(result)
|
91
|
+
raise Halt.new(result)
|
92
|
+
end
|
104
93
|
|
105
|
-
|
94
|
+
# @private
|
95
|
+
def self.coerce_to_monad(ms)
|
96
|
+
return ms if ms.size != 1
|
106
97
|
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
98
|
+
fst = ms[0]
|
99
|
+
|
100
|
+
case fst
|
101
|
+
when Array, List
|
102
|
+
list = fst.is_a?(Array) ? List.coerce(fst) : fst
|
103
|
+
[list.traverse]
|
104
|
+
else
|
105
|
+
ms
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
# @private
|
110
|
+
def self.wrap_method(target, method)
|
111
|
+
target.module_eval(<<-RUBY, __FILE__, __LINE__ + 1)
|
112
|
+
def #{ method }(*)
|
113
|
+
if block_given?
|
114
|
+
super
|
111
115
|
else
|
112
|
-
ms
|
116
|
+
super do |*ms|
|
117
|
+
ms = Do.coerce_to_monad(ms)
|
118
|
+
unwrapped = ms.map { |r|
|
119
|
+
m = r.to_monad
|
120
|
+
m.or { Do.halt(m) }.value!
|
121
|
+
}
|
122
|
+
ms.size == 1 ? unwrapped[0] : unwrapped
|
123
|
+
end
|
113
124
|
end
|
125
|
+
rescue Halt => e
|
126
|
+
e.result
|
114
127
|
end
|
115
|
-
|
128
|
+
RUBY
|
116
129
|
end
|
117
130
|
end
|
118
131
|
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require 'dry/monads/do'
|
2
|
+
|
3
|
+
module Dry
|
4
|
+
module Monads
|
5
|
+
module Do
|
6
|
+
# Do::All automatically wraps methods defined in a class with an unwrapping block.
|
7
|
+
# Similar to what `Do.for(...)` does except wraps every method so you don't have
|
8
|
+
# to list them explicitly.
|
9
|
+
#
|
10
|
+
# @example annotated example
|
11
|
+
#
|
12
|
+
# require 'dry/monads/do/all'
|
13
|
+
# require 'dry/monads/result'
|
14
|
+
#
|
15
|
+
# class CreateUser
|
16
|
+
# include Dry::Monads::Do::All
|
17
|
+
# include Dry::Monads::Result::Mixin
|
18
|
+
#
|
19
|
+
# def call(params)
|
20
|
+
# # Unwrap a monadic value using an implicitly passed block
|
21
|
+
# # if `validates` returns Failure, the execution will be halted
|
22
|
+
# values = yield validate(params)
|
23
|
+
# user = create_user(values)
|
24
|
+
# # If another block is passed to a method then takes
|
25
|
+
# # precedence over the unwrapping block
|
26
|
+
# safely_subscribe(values[:email]) { Logger.info("Already subscribed") }
|
27
|
+
#
|
28
|
+
# Success(user)
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
# def validate(params)
|
32
|
+
# if params.key?(:email)
|
33
|
+
# Success(email: params[:email])
|
34
|
+
# else
|
35
|
+
# Failure(:no_email)
|
36
|
+
# end
|
37
|
+
# end
|
38
|
+
#
|
39
|
+
# def create_user(user)
|
40
|
+
# # Here a block is passed to the method but we don't use it
|
41
|
+
# UserRepo.new.add(user)
|
42
|
+
# end
|
43
|
+
#
|
44
|
+
# def safely_subscribe(email)
|
45
|
+
# repo = SubscriptionRepo.new
|
46
|
+
#
|
47
|
+
# if repo.subscribed?(email)
|
48
|
+
# # This calls the logger because a block
|
49
|
+
# # explicitly passed from `call`
|
50
|
+
# yield
|
51
|
+
# else
|
52
|
+
# repo.subscribe(email)
|
53
|
+
# end
|
54
|
+
# end
|
55
|
+
# end
|
56
|
+
#
|
57
|
+
module All
|
58
|
+
# @private
|
59
|
+
class MethodTracker < Module
|
60
|
+
attr_reader :wrappers
|
61
|
+
|
62
|
+
def initialize(wrappers)
|
63
|
+
super()
|
64
|
+
|
65
|
+
@wrappers = wrappers
|
66
|
+
tracker = self
|
67
|
+
|
68
|
+
module_eval do
|
69
|
+
define_method(:method_added) do |method|
|
70
|
+
super(method)
|
71
|
+
|
72
|
+
tracker.wrap_method(method)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def extend_object(target)
|
78
|
+
super
|
79
|
+
target.prepend(wrappers)
|
80
|
+
end
|
81
|
+
|
82
|
+
def wrap_method(method)
|
83
|
+
Do.wrap_method(wrappers, method)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# @private
|
88
|
+
def self.included(base)
|
89
|
+
super
|
90
|
+
|
91
|
+
tracker = MethodTracker.new(Module.new)
|
92
|
+
base.extend(tracker)
|
93
|
+
base.instance_methods(false).each { |m| tracker.wrap_method(m) }
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
data/lib/dry/monads/lazy.rb
CHANGED
@@ -60,6 +60,9 @@ module Dry
|
|
60
60
|
# @see Dry::Monads::Lazy
|
61
61
|
Lazy = Lazy
|
62
62
|
|
63
|
+
# @see Dry::Monads::Unit
|
64
|
+
Unit = Unit
|
65
|
+
|
63
66
|
# Lazy constructors
|
64
67
|
module Constructors
|
65
68
|
# Lazy computation contructor
|
@@ -74,5 +77,7 @@ module Dry
|
|
74
77
|
include Constructors
|
75
78
|
end
|
76
79
|
end
|
80
|
+
|
81
|
+
extend Lazy::Mixin::Constructors
|
77
82
|
end
|
78
83
|
end
|
data/lib/dry/monads/list.rb
CHANGED
@@ -1,5 +1,4 @@
|
|
1
1
|
require 'dry/equalizer'
|
2
|
-
require 'dry/core/deprecations'
|
3
2
|
|
4
3
|
require 'dry/monads/maybe'
|
5
4
|
require 'dry/monads/task'
|
@@ -232,7 +231,7 @@ module Dry
|
|
232
231
|
coerce(value.drop(1))
|
233
232
|
end
|
234
233
|
|
235
|
-
# Turns the list into a
|
234
|
+
# Turns the list into a typed one.
|
236
235
|
# Type is required for some operations like .traverse.
|
237
236
|
#
|
238
237
|
# @param type [Monad] Monad instance
|
data/lib/dry/monads/maybe.rb
CHANGED
@@ -216,5 +216,78 @@ module Dry
|
|
216
216
|
include Constructors
|
217
217
|
end
|
218
218
|
end
|
219
|
+
|
220
|
+
extend Maybe::Mixin::Constructors
|
221
|
+
|
222
|
+
# @see Maybe::Some
|
223
|
+
Some = Maybe::Some
|
224
|
+
# @see Maybe::None
|
225
|
+
None = Maybe::None
|
226
|
+
|
227
|
+
class Result
|
228
|
+
class Success < Result
|
229
|
+
# @return [Maybe::Some]
|
230
|
+
def to_maybe
|
231
|
+
Kernel.warn 'Success(nil) transformed to None' if @value.nil?
|
232
|
+
Dry::Monads::Maybe(@value)
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
class Failure < Result
|
237
|
+
# @return [Maybe::None]
|
238
|
+
def to_maybe
|
239
|
+
Maybe::None.new(trace)
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
class Task
|
245
|
+
# Converts to Maybe. Blocks the current thread if required.
|
246
|
+
#
|
247
|
+
# @return [Maybe]
|
248
|
+
def to_maybe
|
249
|
+
if promise.wait.fulfilled?
|
250
|
+
Maybe::Some.new(promise.value)
|
251
|
+
else
|
252
|
+
Maybe::None.new(RightBiased::Left.trace_caller)
|
253
|
+
end
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
class Try
|
258
|
+
class Value < Try
|
259
|
+
# @return [Maybe]
|
260
|
+
def to_maybe
|
261
|
+
Dry::Monads::Maybe(@value)
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
class Error < Try
|
266
|
+
# @return [Maybe::None]
|
267
|
+
def to_maybe
|
268
|
+
Maybe::None.new(RightBiased::Left.trace_caller)
|
269
|
+
end
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
class Validated
|
274
|
+
class Valid < Validated
|
275
|
+
# Converts to Maybe::Some
|
276
|
+
#
|
277
|
+
# @return [Maybe::Some]
|
278
|
+
def to_maybe
|
279
|
+
Maybe.pure(value!)
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
class Invalid < Validated
|
284
|
+
# Converts to Maybe::None
|
285
|
+
#
|
286
|
+
# @return [Maybe::None]
|
287
|
+
def to_maybe
|
288
|
+
Maybe::None.new(RightBiased::Left.trace_caller)
|
289
|
+
end
|
290
|
+
end
|
291
|
+
end
|
219
292
|
end
|
220
293
|
end
|
data/lib/dry/monads/result.rb
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
require 'dry/equalizer'
|
2
|
-
require 'dry/core/constants'
|
3
2
|
|
3
|
+
require 'dry/monads/undefined'
|
4
4
|
require 'dry/monads/right_biased'
|
5
5
|
require 'dry/monads/transformer'
|
6
|
-
require 'dry/monads/
|
6
|
+
require 'dry/monads/conversion_stubs'
|
7
7
|
|
8
8
|
module Dry
|
9
9
|
module Monads
|
@@ -12,6 +12,7 @@ module Dry
|
|
12
12
|
# @api public
|
13
13
|
class Result
|
14
14
|
include Transformer
|
15
|
+
include ConversionStubs[:to_maybe, :to_validated]
|
15
16
|
|
16
17
|
# @return [Object] Successful result
|
17
18
|
attr_reader :success
|
@@ -107,25 +108,12 @@ module Dry
|
|
107
108
|
end
|
108
109
|
alias_method :inspect, :to_s
|
109
110
|
|
110
|
-
# @return [Maybe::Some]
|
111
|
-
def to_maybe
|
112
|
-
Kernel.warn 'Success(nil) transformed to None' if @value.nil?
|
113
|
-
Dry::Monads::Maybe(@value)
|
114
|
-
end
|
115
|
-
|
116
111
|
# Transforms to a Failure instance
|
117
112
|
#
|
118
113
|
# @return [Result::Failure]
|
119
114
|
def flip
|
120
115
|
Failure.new(@value, RightBiased::Left.trace_caller)
|
121
116
|
end
|
122
|
-
|
123
|
-
# Transforms to Validated
|
124
|
-
#
|
125
|
-
# @return [Validated::Valid]
|
126
|
-
def to_validated
|
127
|
-
Validated::Valid.new(value!)
|
128
|
-
end
|
129
117
|
end
|
130
118
|
|
131
119
|
# Represents a value of a failed operation.
|
@@ -206,11 +194,6 @@ module Dry
|
|
206
194
|
end
|
207
195
|
alias_method :inspect, :to_s
|
208
196
|
|
209
|
-
# @return [Maybe::None]
|
210
|
-
def to_maybe
|
211
|
-
Maybe::None.new(trace)
|
212
|
-
end
|
213
|
-
|
214
197
|
# Transform to a Success instance
|
215
198
|
#
|
216
199
|
# @return [Result::Success]
|
@@ -232,13 +215,6 @@ module Dry
|
|
232
215
|
def ===(other)
|
233
216
|
Failure === other && failure === other.failure
|
234
217
|
end
|
235
|
-
|
236
|
-
# Transforms to Validated
|
237
|
-
#
|
238
|
-
# @return [Validated::Valid]
|
239
|
-
def to_validated
|
240
|
-
Validated::Invalid.new(failure, trace)
|
241
|
-
end
|
242
218
|
end
|
243
219
|
|
244
220
|
# A module that can be included for easier access to Result monads.
|
@@ -290,5 +266,96 @@ module Dry
|
|
290
266
|
include Constructors
|
291
267
|
end
|
292
268
|
end
|
269
|
+
|
270
|
+
extend Result::Mixin::Constructors
|
271
|
+
|
272
|
+
# @see Result::Success
|
273
|
+
Success = Result::Success
|
274
|
+
# @see Result::Failure
|
275
|
+
Failure = Result::Failure
|
276
|
+
|
277
|
+
# Creates a module that has two methods: `Success` and `Failure`.
|
278
|
+
# `Success` is identical to {Result::Mixin::Constructors#Success} and Failure
|
279
|
+
# rejects values that don't conform the value of the `error`
|
280
|
+
# parameter. This is essentially a Result type with the `Failure` part
|
281
|
+
# fixed.
|
282
|
+
#
|
283
|
+
# @example using dry-types
|
284
|
+
# module Types
|
285
|
+
# include Dry::Types.module
|
286
|
+
# end
|
287
|
+
#
|
288
|
+
# class Operation
|
289
|
+
# # :user_not_found and :account_not_found are the only
|
290
|
+
# # values allowed as failure results
|
291
|
+
# Error =
|
292
|
+
# Types.Value(:user_not_found) |
|
293
|
+
# Types.Value(:account_not_found)
|
294
|
+
#
|
295
|
+
# def find_account(id)
|
296
|
+
# account = acount_repo.find(id)
|
297
|
+
#
|
298
|
+
# account ? Success(account) : Failure(:account_not_found)
|
299
|
+
# end
|
300
|
+
#
|
301
|
+
# def find_user(id)
|
302
|
+
# # ...
|
303
|
+
# end
|
304
|
+
# end
|
305
|
+
#
|
306
|
+
# @param error [#===] the type of allowed failures
|
307
|
+
# @return [Module]
|
308
|
+
def self.Result(error, **options)
|
309
|
+
Result::Fixed[error, **options]
|
310
|
+
end
|
311
|
+
|
312
|
+
class Task
|
313
|
+
# Converts to Result. Blocks the current thread if required.
|
314
|
+
#
|
315
|
+
# @return [Result]
|
316
|
+
def to_result
|
317
|
+
if promise.wait.fulfilled?
|
318
|
+
Result::Success.new(promise.value)
|
319
|
+
else
|
320
|
+
Result::Failure.new(promise.reason, RightBiased::Left.trace_caller)
|
321
|
+
end
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
325
|
+
class Try
|
326
|
+
class Value < Try
|
327
|
+
# @return [Result::Success]
|
328
|
+
def to_result
|
329
|
+
Dry::Monads::Result::Success.new(@value)
|
330
|
+
end
|
331
|
+
end
|
332
|
+
|
333
|
+
class Error < Try
|
334
|
+
# @return [Result::Failure]
|
335
|
+
def to_result
|
336
|
+
Result::Failure.new(exception, RightBiased::Left.trace_caller)
|
337
|
+
end
|
338
|
+
end
|
339
|
+
end
|
340
|
+
|
341
|
+
class Validated
|
342
|
+
class Valid < Validated
|
343
|
+
# Converts to Result::Success
|
344
|
+
#
|
345
|
+
# @return [Result::Success]
|
346
|
+
def to_result
|
347
|
+
Result.pure(value!)
|
348
|
+
end
|
349
|
+
end
|
350
|
+
|
351
|
+
class Invalid < Validated
|
352
|
+
# Concerts to Result::Failure
|
353
|
+
#
|
354
|
+
# @return [Result::Failure]
|
355
|
+
def to_result
|
356
|
+
Result::Failure.new(error, RightBiased::Left.trace_caller)
|
357
|
+
end
|
358
|
+
end
|
359
|
+
end
|
293
360
|
end
|
294
361
|
end
|
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'dry/core/constants'
|
2
2
|
|
3
|
+
require 'dry/monads/unit'
|
3
4
|
require 'dry/monads/curry'
|
4
5
|
require 'dry/monads/errors'
|
5
6
|
|
@@ -99,13 +100,13 @@ module Dry
|
|
99
100
|
# otherwise returns the argument.
|
100
101
|
#
|
101
102
|
# @example happy path
|
102
|
-
# create_user = Dry::Monads::
|
103
|
-
# name =
|
103
|
+
# create_user = Dry::Monads::Success(CreateUser.new)
|
104
|
+
# name = Success("John")
|
104
105
|
# create_user.apply(name) # equivalent to CreateUser.new.call("John")
|
105
106
|
#
|
106
107
|
# @example unhappy path
|
107
|
-
# name =
|
108
|
-
# create_user.apply(name) # =>
|
108
|
+
# name = Failure(:name_missing)
|
109
|
+
# create_user.apply(name) # => Failure(:name_missing)
|
109
110
|
#
|
110
111
|
# @return [RightBiased::Left,RightBiased::Right]
|
111
112
|
def apply(val = Undefined)
|
@@ -116,12 +117,24 @@ module Dry
|
|
116
117
|
Undefined.default(val) { yield }.fmap { |unwrapped| curry.(unwrapped) }
|
117
118
|
end
|
118
119
|
|
119
|
-
# @param other [
|
120
|
+
# @param other [Object]
|
120
121
|
# @return [Boolean]
|
121
122
|
def ===(other)
|
122
123
|
self.class == other.class && value! === other.value!
|
123
124
|
end
|
124
125
|
|
126
|
+
# Maps the value to Dry::Monads::Unit, useful when you don't care
|
127
|
+
# about the actual value.
|
128
|
+
#
|
129
|
+
# @example
|
130
|
+
# Dry::Monads::Success(:success).discard
|
131
|
+
# # => Success(Unit)
|
132
|
+
#
|
133
|
+
# @return [RightBiased::Right]
|
134
|
+
def discard
|
135
|
+
fmap { Unit }
|
136
|
+
end
|
137
|
+
|
125
138
|
private
|
126
139
|
|
127
140
|
# @api private
|
@@ -215,6 +228,14 @@ module Dry
|
|
215
228
|
def apply(*)
|
216
229
|
self
|
217
230
|
end
|
231
|
+
|
232
|
+
# Returns self back. It exists to keep the interface
|
233
|
+
# identical to that of {RightBiased::Right}.
|
234
|
+
#
|
235
|
+
# @return [RightBiased::Left]
|
236
|
+
def discard
|
237
|
+
fmap { Unit }
|
238
|
+
end
|
218
239
|
end
|
219
240
|
end
|
220
241
|
end
|
data/lib/dry/monads/task.rb
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
require 'concurrent/promise'
|
2
2
|
|
3
|
+
require 'dry/monads/unit'
|
3
4
|
require 'dry/monads/curry'
|
5
|
+
require 'dry/monads/conversion_stubs'
|
4
6
|
|
5
7
|
module Dry
|
6
8
|
module Monads
|
@@ -68,6 +70,8 @@ module Dry
|
|
68
70
|
end
|
69
71
|
end
|
70
72
|
|
73
|
+
include ConversionStubs[:to_maybe, :to_result]
|
74
|
+
|
71
75
|
# @api private
|
72
76
|
attr_reader :promise
|
73
77
|
protected :promise
|
@@ -112,28 +116,6 @@ module Dry
|
|
112
116
|
end
|
113
117
|
alias_method :then, :bind
|
114
118
|
|
115
|
-
# Converts to Result. Blocks the current thread if required.
|
116
|
-
#
|
117
|
-
# @return [Result]
|
118
|
-
def to_result
|
119
|
-
if promise.wait.fulfilled?
|
120
|
-
Result::Success.new(promise.value)
|
121
|
-
else
|
122
|
-
Result::Failure.new(promise.reason, RightBiased::Left.trace_caller)
|
123
|
-
end
|
124
|
-
end
|
125
|
-
|
126
|
-
# Converts to Maybe. Blocks the current thread if required.
|
127
|
-
#
|
128
|
-
# @return [Maybe]
|
129
|
-
def to_maybe
|
130
|
-
if promise.wait.fulfilled?
|
131
|
-
Maybe::Some.new(promise.value)
|
132
|
-
else
|
133
|
-
Maybe::None.new(RightBiased::Left.trace_caller)
|
134
|
-
end
|
135
|
-
end
|
136
|
-
|
137
119
|
# @return [String]
|
138
120
|
def to_s
|
139
121
|
state = case promise.state
|
@@ -244,6 +226,13 @@ module Dry
|
|
244
226
|
bind { |f| arg.fmap { |v| curry(f).(v) } }
|
245
227
|
end
|
246
228
|
|
229
|
+
# Maps a successful result to Unit, effectively discards it
|
230
|
+
#
|
231
|
+
# @return [Task]
|
232
|
+
def discard
|
233
|
+
fmap { Unit }
|
234
|
+
end
|
235
|
+
|
247
236
|
private
|
248
237
|
|
249
238
|
# @api private
|
@@ -271,7 +260,11 @@ module Dry
|
|
271
260
|
#
|
272
261
|
# @api public
|
273
262
|
module Mixin
|
274
|
-
|
263
|
+
# @private
|
264
|
+
Task = Task
|
265
|
+
|
266
|
+
# @see Dry::Monads::Unit
|
267
|
+
Unit = Unit # @private
|
275
268
|
|
276
269
|
# Created a mixin with the given executor injected.
|
277
270
|
#
|
@@ -305,5 +298,7 @@ module Dry
|
|
305
298
|
include Constructors
|
306
299
|
end
|
307
300
|
end
|
301
|
+
|
302
|
+
extend Task::Mixin::Constructors
|
308
303
|
end
|
309
304
|
end
|
data/lib/dry/monads/try.rb
CHANGED
@@ -2,8 +2,7 @@ require 'dry/equalizer'
|
|
2
2
|
require 'dry/core/deprecations'
|
3
3
|
|
4
4
|
require 'dry/monads/right_biased'
|
5
|
-
require 'dry/monads/
|
6
|
-
require 'dry/monads/maybe'
|
5
|
+
require 'dry/monads/conversion_stubs'
|
7
6
|
|
8
7
|
module Dry
|
9
8
|
module Monads
|
@@ -15,6 +14,8 @@ module Dry
|
|
15
14
|
# @private
|
16
15
|
DEFAULT_EXCEPTIONS = [StandardError].freeze
|
17
16
|
|
17
|
+
include ConversionStubs[:to_maybe, :to_result]
|
18
|
+
|
18
19
|
# @return [Exception] Caught exception
|
19
20
|
attr_reader :exception
|
20
21
|
|
@@ -153,16 +154,6 @@ module Dry
|
|
153
154
|
Error.new(e)
|
154
155
|
end
|
155
156
|
|
156
|
-
# @return [Maybe]
|
157
|
-
def to_maybe
|
158
|
-
Dry::Monads::Maybe(@value)
|
159
|
-
end
|
160
|
-
|
161
|
-
# @return [Result::Success]
|
162
|
-
def to_result
|
163
|
-
Dry::Monads::Result::Success.new(@value)
|
164
|
-
end
|
165
|
-
|
166
157
|
# @return [String]
|
167
158
|
def to_s
|
168
159
|
"Try::Value(#{ @value.inspect })"
|
@@ -182,16 +173,6 @@ module Dry
|
|
182
173
|
@exception = exception
|
183
174
|
end
|
184
175
|
|
185
|
-
# @return [Maybe::None]
|
186
|
-
def to_maybe
|
187
|
-
Maybe::None.new(RightBiased::Left.trace_caller)
|
188
|
-
end
|
189
|
-
|
190
|
-
# @return [Result::Failure]
|
191
|
-
def to_result
|
192
|
-
Result::Failure.new(exception, RightBiased::Left.trace_caller)
|
193
|
-
end
|
194
|
-
|
195
176
|
# @return [String]
|
196
177
|
def to_s
|
197
178
|
"Try::Error(#{ exception.class }: #{ exception.message })"
|
@@ -291,5 +272,7 @@ module Dry
|
|
291
272
|
end
|
292
273
|
end
|
293
274
|
end
|
275
|
+
|
276
|
+
extend Try::Mixin::Constructors
|
294
277
|
end
|
295
278
|
end
|
data/lib/dry/monads/validated.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
|
-
require 'dry/monads/
|
2
|
-
require 'dry/monads/
|
1
|
+
require 'dry/monads/conversion_stubs'
|
2
|
+
require 'dry/monads/undefined'
|
3
|
+
require 'dry/monads/right_biased'
|
3
4
|
|
4
5
|
module Dry
|
5
6
|
module Monads
|
@@ -19,6 +20,8 @@ module Dry
|
|
19
20
|
# # => Valid(List['London', 'John'])
|
20
21
|
#
|
21
22
|
class Validated
|
23
|
+
include ConversionStubs[:to_maybe, :to_result]
|
24
|
+
|
22
25
|
class << self
|
23
26
|
# Wraps a value with `Valid`.
|
24
27
|
#
|
@@ -121,18 +124,10 @@ module Dry
|
|
121
124
|
end
|
122
125
|
alias_method :to_s, :inspect
|
123
126
|
|
124
|
-
#
|
125
|
-
#
|
126
|
-
|
127
|
-
|
128
|
-
Maybe.pure(value!)
|
129
|
-
end
|
130
|
-
|
131
|
-
# Converts to Result::Success
|
132
|
-
#
|
133
|
-
# @return [Result::Success]
|
134
|
-
def to_result
|
135
|
-
Result.pure(value!)
|
127
|
+
# @param other [Object]
|
128
|
+
# @return [Boolean]
|
129
|
+
def ===(other)
|
130
|
+
self.class == other.class && value! === other.value!
|
136
131
|
end
|
137
132
|
end
|
138
133
|
|
@@ -213,18 +208,10 @@ module Dry
|
|
213
208
|
end
|
214
209
|
alias_method :to_s, :inspect
|
215
210
|
|
216
|
-
#
|
217
|
-
#
|
218
|
-
|
219
|
-
|
220
|
-
Maybe::None.new(RightBiased::Left.trace_caller)
|
221
|
-
end
|
222
|
-
|
223
|
-
# Concerts to Result::Failure
|
224
|
-
#
|
225
|
-
# @return [Result::Failure]
|
226
|
-
def to_result
|
227
|
-
Result::Failure.new(error, RightBiased::Left.trace_caller)
|
211
|
+
# @param other [Object]
|
212
|
+
# @return [Boolean]
|
213
|
+
def ===(other)
|
214
|
+
self.class == other.class && error === other.error
|
228
215
|
end
|
229
216
|
end
|
230
217
|
|
@@ -279,5 +266,32 @@ module Dry
|
|
279
266
|
include Constructors
|
280
267
|
end
|
281
268
|
end
|
269
|
+
|
270
|
+
extend Validated::Mixin::Constructors
|
271
|
+
|
272
|
+
# @see Validated::Valid
|
273
|
+
Valid = Validated::Valid
|
274
|
+
# @see Validated::Invalid
|
275
|
+
Invalid = Validated::Invalid
|
276
|
+
|
277
|
+
class Result
|
278
|
+
class Success < Result
|
279
|
+
# Transforms to Validated
|
280
|
+
#
|
281
|
+
# @return [Validated::Valid]
|
282
|
+
def to_validated
|
283
|
+
Validated::Valid.new(value!)
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
class Failure < Result
|
288
|
+
# Transforms to Validated
|
289
|
+
#
|
290
|
+
# @return [Validated::Valid]
|
291
|
+
def to_validated
|
292
|
+
Validated::Invalid.new(failure, trace)
|
293
|
+
end
|
294
|
+
end
|
295
|
+
end
|
282
296
|
end
|
283
297
|
end
|
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.0.0.
|
4
|
+
version: 1.0.0.beta2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nikita Shilnikov
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-
|
11
|
+
date: 2018-05-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: dry-equalizer
|
@@ -137,8 +137,11 @@ files:
|
|
137
137
|
- dry-monads.gemspec
|
138
138
|
- lib/dry-monads.rb
|
139
139
|
- lib/dry/monads.rb
|
140
|
+
- lib/dry/monads/all.rb
|
141
|
+
- lib/dry/monads/conversion_stubs.rb
|
140
142
|
- lib/dry/monads/curry.rb
|
141
143
|
- lib/dry/monads/do.rb
|
144
|
+
- lib/dry/monads/do/all.rb
|
142
145
|
- lib/dry/monads/either.rb
|
143
146
|
- lib/dry/monads/errors.rb
|
144
147
|
- lib/dry/monads/lazy.rb
|
@@ -151,6 +154,8 @@ files:
|
|
151
154
|
- lib/dry/monads/transformer.rb
|
152
155
|
- lib/dry/monads/traverse.rb
|
153
156
|
- lib/dry/monads/try.rb
|
157
|
+
- lib/dry/monads/undefined.rb
|
158
|
+
- lib/dry/monads/unit.rb
|
154
159
|
- lib/dry/monads/validated.rb
|
155
160
|
- lib/dry/monads/version.rb
|
156
161
|
- lib/json/add/dry/monads/maybe.rb
|