dry-monads 0.4.0 → 1.0.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.travis.yml +7 -4
- data/CHANGELOG.md +141 -1
- data/CONTRIBUTING.md +1 -1
- data/LICENSE +1 -1
- data/README.md +2 -2
- data/dry-monads.gemspec +2 -1
- data/lib/dry/monads.rb +22 -1
- data/lib/dry/monads/curry.rb +19 -0
- data/lib/dry/monads/do.rb +119 -0
- data/lib/dry/monads/either.rb +63 -0
- data/lib/dry/monads/errors.rb +3 -1
- data/lib/dry/monads/lazy.rb +78 -0
- data/lib/dry/monads/list.rb +109 -12
- data/lib/dry/monads/maybe.rb +50 -24
- data/lib/dry/monads/result.rb +93 -49
- data/lib/dry/monads/result/fixed.rb +2 -2
- data/lib/dry/monads/right_biased.rb +18 -27
- data/lib/dry/monads/task.rb +309 -0
- data/lib/dry/monads/transformer.rb +1 -0
- data/lib/dry/monads/traverse.rb +20 -0
- data/lib/dry/monads/try.rb +111 -39
- data/lib/dry/monads/validated.rb +283 -0
- data/lib/dry/monads/version.rb +2 -1
- data/lib/json/add/dry/monads/maybe.rb +1 -1
- metadata +29 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: e39ab380c859836fb98aad8bc158eeafb73896472cc703254c0f21490c6cde68
|
4
|
+
data.tar.gz: 749f9cf55040d0cafc8f8f4e3d8c60a8e2486768223069adff7f0b4c55cc9a43
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3c66d5ea73e5c781e26acfb1611067a34cc25a9fbbb258a01ad8f5705b33cd569944ae9eab126e18456b4f6eeec0722163e39406e52c332e5224a78b5dae53e9
|
7
|
+
data.tar.gz: 73a8d308f628a5ea630cabdaf240f67627e3310ec3cf64a75cec7140b6c5645853cfcf50ecc463a38f1813c28b78a145d7d40ec45998327c7cbedce1177c60c5
|
data/.travis.yml
CHANGED
@@ -5,13 +5,16 @@ cache: bundler
|
|
5
5
|
bundler_args: --without benchmarks docs
|
6
6
|
script:
|
7
7
|
- bundle exec rake spec
|
8
|
+
before_install:
|
9
|
+
- gem update --system
|
8
10
|
after_success:
|
9
11
|
- '[ -d coverage ] && bundle exec codeclimate-test-reporter'
|
10
12
|
rvm:
|
11
|
-
- 2.2.
|
12
|
-
- 2.3.
|
13
|
-
- 2.4.
|
14
|
-
-
|
13
|
+
- 2.2.9
|
14
|
+
- 2.3.6
|
15
|
+
- 2.4.3
|
16
|
+
- 2.5.0
|
17
|
+
- jruby-9.1.15.0
|
15
18
|
- ruby-head
|
16
19
|
env:
|
17
20
|
global:
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,143 @@
|
|
1
|
+
# v1.0.0 to-be-released
|
2
|
+
|
3
|
+
## Added
|
4
|
+
|
5
|
+
* `do`-like notation (the idea comes from Haskell of course). This is the biggest and most important addition in 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
|
+
|
7
|
+
```ruby
|
8
|
+
class CreateUser
|
9
|
+
include Dry::Monads
|
10
|
+
include Dry::Monads::Do.for(:call)
|
11
|
+
|
12
|
+
attr_reader :user_repo
|
13
|
+
|
14
|
+
def initialize(:user_repo)
|
15
|
+
@user_repo = user_repo
|
16
|
+
end
|
17
|
+
|
18
|
+
def call(params)
|
19
|
+
json = yield parse_json(params)
|
20
|
+
hash = yield validate(json)
|
21
|
+
|
22
|
+
user_repo.transaction do
|
23
|
+
user = yield create_user(hash[:user])
|
24
|
+
yield create_profile(user, hash[:profile])
|
25
|
+
|
26
|
+
Success(user)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def parse_json(params)
|
33
|
+
Try[JSON::ParserError] {
|
34
|
+
JSON.parse(params)
|
35
|
+
}.to_result
|
36
|
+
end
|
37
|
+
|
38
|
+
def validate(json)
|
39
|
+
UserSchema.(json).to_monad
|
40
|
+
end
|
41
|
+
|
42
|
+
def create_user(user_data)
|
43
|
+
Try[Sequel::Error] { user_repo.create(user_data) }.to_result
|
44
|
+
end
|
45
|
+
|
46
|
+
def create_profile(user, profile_data)
|
47
|
+
Try[Sequel::Error] {
|
48
|
+
user_repo.create_profile(user, profile_data)
|
49
|
+
}.to_result
|
50
|
+
end
|
51
|
+
end
|
52
|
+
```
|
53
|
+
|
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
|
+
|
56
|
+
* The `Task` monad based on `Promise` from the [`concurrent-ruby` gem](https://github.com/ruby-concurrency/concurrent-ruby/). `Task` represents an asynchrounos computation which _can be_ (doesn't have to!) run on a seperated 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
|
+
|
58
|
+
```ruby
|
59
|
+
include Dry::Monads::Task::Mixin
|
60
|
+
|
61
|
+
def call
|
62
|
+
name = Task { get_name_via_http } # runs a request in the background
|
63
|
+
email = Task { get_email_via_http } # runs another one request in the background
|
64
|
+
|
65
|
+
# to_result forces both computations/requests to complete by pausing current thread
|
66
|
+
# returns `Result::Success/Result::Failure`
|
67
|
+
name.bind { |n| email.fmap { |e| create(e, n) } }.to_result
|
68
|
+
end
|
69
|
+
```
|
70
|
+
|
71
|
+
`Task` works perfectly with `Do`
|
72
|
+
|
73
|
+
```ruby
|
74
|
+
include Dry::Monads::Do.for(:call)
|
75
|
+
|
76
|
+
def call
|
77
|
+
name, email = yield Task { get_name_via_http }, Task { get_email_via_http }
|
78
|
+
Success(create(e, n))
|
79
|
+
end
|
80
|
+
```
|
81
|
+
|
82
|
+
* `Lazy` is a copy of `Task` that isn't run until you ask for the value _for the first time_. It is guaranteed the evaluation is run at most once as opposed to lazy assignment `||=` which isn't synchronized. `Lazy` is run on the same thread asking for the value (flash-gordon)
|
83
|
+
|
84
|
+
* Automatic type inference with `.typed` for lists was deprecated. Instead, typed list builders were added
|
85
|
+
|
86
|
+
```ruby
|
87
|
+
list = List::Task[Task { get_name }, Task { get_email }]
|
88
|
+
list.traverse # => Task(List['John', 'john@doe.org'])
|
89
|
+
```
|
90
|
+
|
91
|
+
The code above runs two tasks in parallel and automatically combines their results with `traverse` (flash-gordon)
|
92
|
+
|
93
|
+
* `Try` got a new call syntax supported in Ruby 2.5+
|
94
|
+
|
95
|
+
```ruby
|
96
|
+
Try[ArgumentError, TypeError] { unsafe_operation }
|
97
|
+
```
|
98
|
+
|
99
|
+
Prior to 2.5, it wasn't possible to pass a block to `[]`.
|
100
|
+
|
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.
|
102
|
+
|
103
|
+
```ruby
|
104
|
+
include Dry::Monads
|
105
|
+
include Dry::Monads::Do.for(:call)
|
106
|
+
|
107
|
+
def call(input)
|
108
|
+
name, email = yield [
|
109
|
+
validate_name(input[:name]),
|
110
|
+
validate_email(input[:email])
|
111
|
+
]
|
112
|
+
|
113
|
+
Success(create(name, email))
|
114
|
+
end
|
115
|
+
|
116
|
+
# can return
|
117
|
+
# * Success(User(...))
|
118
|
+
# * Invalid(List[:invalid_name])
|
119
|
+
# * Invalid(List[:invalid_email])
|
120
|
+
# * Invalid(List[:invalid_name, :invalid_email])
|
121
|
+
```
|
122
|
+
|
123
|
+
In the example above an array of `Validated` values is implicitly casted to `List::Validated`. It's supported because it's useful but don't forget it's all about types and 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. On of the biggest downside of dealing wtih 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
|
+
|
127
|
+
```ruby
|
128
|
+
Failure(:invalid_name).trace # => app/operations/create_user.rb:43
|
129
|
+
```
|
130
|
+
|
131
|
+
## Deprecations
|
132
|
+
|
133
|
+
* `Either`, the former name of `Result`, is now deprecated
|
134
|
+
|
135
|
+
## BREAKING CHANGES
|
136
|
+
|
137
|
+
* `Either#value` and `Maybe#value` were both droped, use `value_or` or `value!` when you :100: sure it's safe
|
138
|
+
|
139
|
+
[Compare v0.4.0...v1.0.0](https://github.com/dry-rb/dry-monads/compare/v0.4.0...master)
|
140
|
+
|
1
141
|
# v0.4.0 2017-11-11
|
2
142
|
|
3
143
|
## Changed
|
@@ -23,7 +163,7 @@
|
|
23
163
|
|
24
164
|
## Deprecated
|
25
165
|
|
26
|
-
* Direct accessing `value` on right-biased monads has been deprecated, use the `value!` method instead. `value!` will raise an exception if it is called on a
|
166
|
+
* Direct accessing `value` on right-biased monads has been deprecated, use the `value!` method instead. `value!` will raise an exception if it is called on a Failure/None/Error instance (flash-gordon)
|
27
167
|
|
28
168
|
[Compare v0.3.1...v0.4.0](https://github.com/dry-rb/dry-monads/compare/v0.3.1...v0.4.0)
|
29
169
|
|
data/CONTRIBUTING.md
CHANGED
@@ -22,7 +22,7 @@ Other requirements:
|
|
22
22
|
2) Follow the style conventions of the surrounding code. In most cases, this is standard ruby style.
|
23
23
|
3) Add API documentation if it's a new feature
|
24
24
|
4) Update API documentation if it changes an existing feature
|
25
|
-
5) Bonus points for sending a PR to [github.com/dry-rb/dry-rb.org](github.com/dry-rb/dry-rb.org) which updates user documentation and guides
|
25
|
+
5) Bonus points for sending a PR to [github.com/dry-rb/dry-rb.org](https://github.com/dry-rb/dry-rb.org) which updates user documentation and guides
|
26
26
|
|
27
27
|
# Asking for help
|
28
28
|
|
data/LICENSE
CHANGED
data/README.md
CHANGED
@@ -8,8 +8,8 @@
|
|
8
8
|
|
9
9
|
[![Gem Version](https://img.shields.io/gem/v/dry-monads.svg)][gem]
|
10
10
|
[![Build Status](https://img.shields.io/travis/dry-rb/dry-monads.svg)][travis]
|
11
|
-
[![Code Climate](https://
|
12
|
-
[![Test Coverage](https://
|
11
|
+
[![Code Climate](https://api.codeclimate.com/v1/badges/b0ea4d8023d53b7f0f50/maintainability)][code_climate]
|
12
|
+
[![Test Coverage](https://api.codeclimate.com/v1/badges/b0ea4d8023d53b7f0f50/test_coverage)][code_climate]
|
13
13
|
[![API Documentation Coverage](http://inch-ci.org/github/dry-rb/dry-monads.svg)][inch]
|
14
14
|
![No monkey-patches](https://img.shields.io/badge/monkey--patches-0-brightgreen.svg)
|
15
15
|
|
data/dry-monads.gemspec
CHANGED
@@ -27,7 +27,8 @@ Gem::Specification.new do |spec|
|
|
27
27
|
spec.require_paths = ['lib']
|
28
28
|
spec.required_ruby_version = ">= 2.2.0"
|
29
29
|
spec.add_dependency 'dry-equalizer'
|
30
|
-
spec.add_dependency 'dry-core', '~> 0.
|
30
|
+
spec.add_dependency 'dry-core', '~> 0.4', '>= 0.4.4'
|
31
|
+
spec.add_dependency 'concurrent-ruby', '~> 1.0'
|
31
32
|
|
32
33
|
spec.add_development_dependency 'bundler'
|
33
34
|
spec.add_development_dependency 'rake'
|
data/lib/dry/monads.rb
CHANGED
@@ -2,26 +2,47 @@ require 'dry/core/constants'
|
|
2
2
|
require 'dry/monads/maybe'
|
3
3
|
require 'dry/monads/try'
|
4
4
|
require 'dry/monads/list'
|
5
|
+
require 'dry/monads/task'
|
6
|
+
require 'dry/monads/lazy'
|
5
7
|
require 'dry/monads/result'
|
6
8
|
require 'dry/monads/result/fixed'
|
9
|
+
require 'dry/monads/do'
|
10
|
+
require 'dry/monads/validated'
|
7
11
|
|
8
12
|
module Dry
|
13
|
+
# Common, idiomatic monads for Ruby
|
14
|
+
#
|
9
15
|
# @api public
|
10
16
|
module Monads
|
17
|
+
# @private
|
11
18
|
Undefined = Dry::Core::Constants::Undefined
|
12
19
|
|
20
|
+
# List of monad constructors
|
13
21
|
CONSTRUCTORS = [
|
14
22
|
Maybe::Mixin::Constructors,
|
15
|
-
Result::Mixin::Constructors
|
23
|
+
Result::Mixin::Constructors,
|
24
|
+
Validated::Mixin::Constructors,
|
25
|
+
Try::Mixin::Constructors,
|
26
|
+
Task::Mixin::Constructors,
|
27
|
+
Lazy::Mixin::Constructors
|
16
28
|
].freeze
|
17
29
|
|
30
|
+
# @see Maybe::Some
|
18
31
|
Some = Maybe::Some
|
32
|
+
# @see Maybe::None
|
19
33
|
None = Maybe::None
|
34
|
+
# @see Result::Success
|
20
35
|
Success = Result::Success
|
36
|
+
# @see Result::Failure
|
21
37
|
Failure = Result::Failure
|
38
|
+
# @see Validated::Valid
|
39
|
+
Valid = Validated::Valid
|
40
|
+
# @see Validated::Invalid
|
41
|
+
Invalid = Validated::Invalid
|
22
42
|
|
23
43
|
extend(*CONSTRUCTORS)
|
24
44
|
|
45
|
+
# @private
|
25
46
|
def self.included(base)
|
26
47
|
super
|
27
48
|
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Dry
|
2
|
+
module Monads
|
3
|
+
# @private
|
4
|
+
module Curry
|
5
|
+
# @private
|
6
|
+
def self.call(value)
|
7
|
+
func = value.is_a?(Proc) ? value : value.method(:call)
|
8
|
+
seq_args = func.parameters.count { |type, _| type == :req || type == :opt }
|
9
|
+
seq_args += 1 if func.parameters.any? { |type, _| type == :keyreq }
|
10
|
+
|
11
|
+
if seq_args > 1
|
12
|
+
func.curry
|
13
|
+
else
|
14
|
+
func
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
module Dry
|
2
|
+
module Monads
|
3
|
+
# An implementation of do-notation.
|
4
|
+
#
|
5
|
+
# @see Do.for
|
6
|
+
module Do
|
7
|
+
# @private
|
8
|
+
class Halt < StandardError
|
9
|
+
# @private
|
10
|
+
attr_reader :result
|
11
|
+
|
12
|
+
def initialize(result)
|
13
|
+
super()
|
14
|
+
|
15
|
+
@result = result
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# Generates a module that passes a block to methods
|
20
|
+
# that either unwraps a single-valued monadic value or halts
|
21
|
+
# the execution.
|
22
|
+
#
|
23
|
+
# @example A complete example
|
24
|
+
#
|
25
|
+
# class CreateUser
|
26
|
+
# include Dry::Monads::Result::Mixin
|
27
|
+
# include Dry::Monads::Try::Mixin
|
28
|
+
# include Dry::Monads::Do.for(:call)
|
29
|
+
#
|
30
|
+
# attr_reader :user_repo
|
31
|
+
#
|
32
|
+
# def initialize(:user_repo)
|
33
|
+
# @user_repo = user_repo
|
34
|
+
# end
|
35
|
+
#
|
36
|
+
# def call(params)
|
37
|
+
# json = yield parse_json(params)
|
38
|
+
# hash = yield validate(json)
|
39
|
+
#
|
40
|
+
# user_repo.transaction do
|
41
|
+
# user = yield create_user(hash[:user])
|
42
|
+
# yield create_profile(user, hash[:profile])
|
43
|
+
# end
|
44
|
+
#
|
45
|
+
# Success(user)
|
46
|
+
# end
|
47
|
+
#
|
48
|
+
# private
|
49
|
+
#
|
50
|
+
# def parse_json(params)
|
51
|
+
# Try(JSON::ParserError) {
|
52
|
+
# JSON.parse(params)
|
53
|
+
# }.to_result
|
54
|
+
# end
|
55
|
+
#
|
56
|
+
# def validate(json)
|
57
|
+
# UserSchema.(json).to_monad
|
58
|
+
# end
|
59
|
+
#
|
60
|
+
# def create_user(user_data)
|
61
|
+
# Try(Sequel::Error) {
|
62
|
+
# user_repo.create(user_data)
|
63
|
+
# }.to_result
|
64
|
+
# end
|
65
|
+
#
|
66
|
+
# def create_profile(user, profile_data)
|
67
|
+
# Try(Sequel::Error) {
|
68
|
+
# user_repo.create_profile(user, profile_data)
|
69
|
+
# }.to_result
|
70
|
+
# end
|
71
|
+
# end
|
72
|
+
#
|
73
|
+
# @param [Array<Symbol>] methods
|
74
|
+
# @return [Module]
|
75
|
+
def self.for(*methods)
|
76
|
+
mod = Module.new do
|
77
|
+
methods.each do |method|
|
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
|
90
|
+
end
|
91
|
+
|
92
|
+
Module.new do
|
93
|
+
singleton_class.send(:define_method, :included) do |base|
|
94
|
+
base.prepend(mod)
|
95
|
+
end
|
96
|
+
|
97
|
+
def halt(result)
|
98
|
+
raise Halt.new(result)
|
99
|
+
end
|
100
|
+
|
101
|
+
# @private
|
102
|
+
def coerce_to_monad(ms)
|
103
|
+
return ms if ms.size != 1
|
104
|
+
|
105
|
+
fst = ms[0]
|
106
|
+
|
107
|
+
case fst
|
108
|
+
when Array, List
|
109
|
+
list = fst.is_a?(Array) ? List.coerce(fst) : fst
|
110
|
+
[list.traverse]
|
111
|
+
else
|
112
|
+
ms
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
data/lib/dry/monads/either.rb
CHANGED
@@ -1 +1,64 @@
|
|
1
|
+
require 'dry/core/deprecations'
|
2
|
+
|
3
|
+
Dry::Core::Deprecations.warn('Either monad was renamed to Result', tag: :'dry-monads')
|
4
|
+
|
1
5
|
require 'dry/monads/result'
|
6
|
+
|
7
|
+
module Dry
|
8
|
+
module Monads
|
9
|
+
Either = Result
|
10
|
+
deprecate_constant :Either
|
11
|
+
|
12
|
+
class Result
|
13
|
+
extend Dry::Core::Deprecations[:'dry-monads']
|
14
|
+
|
15
|
+
deprecate :to_either, :to_result
|
16
|
+
|
17
|
+
Right = Success
|
18
|
+
Left = Failure
|
19
|
+
|
20
|
+
deprecate_constant :Right
|
21
|
+
deprecate_constant :Left
|
22
|
+
|
23
|
+
module Mixin
|
24
|
+
module Constructors
|
25
|
+
extend Dry::Core::Deprecations[:'dry-monads']
|
26
|
+
|
27
|
+
Right = Success
|
28
|
+
Left = Failure
|
29
|
+
deprecate_constant :Right
|
30
|
+
deprecate_constant :Left
|
31
|
+
|
32
|
+
deprecate :Right, :Success
|
33
|
+
deprecate :Left, :Failure
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
class Success
|
38
|
+
deprecate :left?, :failure?
|
39
|
+
deprecate :right?, :success?
|
40
|
+
end
|
41
|
+
|
42
|
+
class Failure
|
43
|
+
deprecate :left?, :failure?
|
44
|
+
deprecate :right?, :success?
|
45
|
+
|
46
|
+
deprecate :left, :failure
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
class Try
|
51
|
+
class Value
|
52
|
+
extend Dry::Core::Deprecations[:'dry-monads']
|
53
|
+
|
54
|
+
deprecate :to_either, :to_result
|
55
|
+
end
|
56
|
+
|
57
|
+
class Error
|
58
|
+
extend Dry::Core::Deprecations[:'dry-monads']
|
59
|
+
|
60
|
+
deprecate :to_either, :to_result
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|