dry-monads 1.1.0 → 1.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c819d8b433c150703fa74c233a2079c524e2841ddeb4acf073fef3e16ef54225
4
- data.tar.gz: b2da0bd96075be4863bb2073b434489ecae1374ebf0cf5cf55ed11a89c07263f
3
+ metadata.gz: ae0ce757dc3f6ec60e8fb1d3e111535fe3e32d40956c5e38cdbef583805b5f6a
4
+ data.tar.gz: 83962c10f05f0a53d37a34c77c4197a799a8a78f7a6de767999a6eebd0f30571
5
5
  SHA512:
6
- metadata.gz: bbb8cb8fc13031cc1b75e9f830381b29cc89ae4652c58a927c45c474ae93bd13364a190cf0af792586fe25f5a9c3d6611e7dbdce01e63e4347d1fe56774201a4
7
- data.tar.gz: 5a116f3c547e15ad14aa7d72894c4cbafb90f893033fa98c141e4e20a2644a48b741e1fcee762e4e417c4c9437803802e441b2ce7d07b4b7791f73f0e4457ca4
6
+ metadata.gz: 1754fea24d90657ea3947de3c1b4206d22c977ec30546377ccb82c166a898ca8f4dc0b6b21dd384ceea78a706c29c9570951222f48e6a40b661964b1f43ffef2
7
+ data.tar.gz: 7667bcb8d8a009f68055c9cade624e7276a51749f42fdbfacc8e47b13763e10f57e7e34b00878017498f100f9eae55de72e10b9d8ffcad8d952043311aea74ed
@@ -7,15 +7,15 @@ script:
7
7
  - bundle exec rake spec
8
8
  before_install:
9
9
  - gem update --system
10
+ - gem install bundler
10
11
  after_success:
11
12
  - '[ -d coverage ] && bundle exec codeclimate-test-reporter'
12
13
  rvm:
13
- - 2.2.10
14
- - 2.3.7
15
- - 2.4.4
16
- - 2.5.1
17
- - jruby-9.2.0.0
18
- - ruby-head
14
+ - 2.3.8
15
+ - 2.4.5
16
+ - 2.5.3
17
+ - 2.6.0
18
+ - jruby-9.2.4.0
19
19
  env:
20
20
  global:
21
21
  - JRUBY_OPTS='--dev -J-Xmx1024M'
@@ -1,3 +1,93 @@
1
+ # v1.2.0 2019-01-12
2
+
3
+ ## BREAKING CHANGES
4
+
5
+ * Support for Ruby 2.2 was dropped. Ruby 2.2 reached its EOL on March 31, 2018.
6
+
7
+ ## Added
8
+
9
+ * Most of constructors now have `call` alias so you can compose them with Procs nicely if you've switched to Ruby 2.6 (flash-gordon)
10
+ ```ruby
11
+ pipe = -> x { x.upcase } >> Success
12
+ pipe.('foo') # => Success('FOO')
13
+ ```
14
+ * `List#collect` gathers `Some` values from the list (flash-gordon)
15
+ ```ruby
16
+ include Dry::Monads::List::Mixin
17
+ include Dry::Monads::Maybe::Mixin
18
+ # ...
19
+ List[10, 5, 0].collect do |divisor|
20
+ if divisor.zero?
21
+ None()
22
+ else
23
+ Some(n / divisor)
24
+ end
25
+ end
26
+ # => List[4, 2]
27
+ ```
28
+
29
+ Without block:
30
+
31
+ ```ruby
32
+ List[Some(5), None(), Some(3)].collect.map { |x| x * 2 }
33
+ # => [10, 6]
34
+ ```
35
+
36
+ * Right-biased monads got `#flatten` and `#and` (falsh-gordon)
37
+
38
+ `#flatten` removes one level of monadic structure, it's useful when you're dealing with things like `Maybe` of `Maybe` of something:
39
+ ```ruby
40
+ include Dry::Monads::Maybe::Mixin
41
+
42
+ Some(Some(1)).flatten # => Some(1)
43
+ Some(None()).flatten # => None
44
+ None().flatten # => None
45
+ ```
46
+ In contrast to `Array#flatten`, dry-monads' version removes only 1 level of nesting, that is always acts as `Array#flatten(1)`:
47
+ ```ruby
48
+ Some(Some(Some(1))).flatten # => Some(Some(1))
49
+ ```
50
+ `#and` is handy for combining two monadic values and working with them at once:
51
+ ```ruby
52
+ include Dry::Monads::Maybe::Mixin
53
+
54
+ # using block
55
+ Some(5).and(Some(3)) { |x, y| x + y } # => Some(8)
56
+ # without block
57
+ Some(5).and(Some(3)) # => Some([5, 3])
58
+ # other cases
59
+ Some(5).and(None()) # => None()
60
+ None().and(Some(5)) # => None()
61
+ ```
62
+
63
+ * Concise imports with `Dry::Monads.[]`. You're no longer required to require all desired monads and include them one-by-one, the `[]` method handles it for you (flash-gordon)
64
+
65
+ ```ruby
66
+ require 'dry/monads'
67
+
68
+ class CreateUser
69
+ include Dry::Monads[:result, :do]
70
+
71
+ def initialize(repo, send_email)
72
+ @repo = repo
73
+ @send_email = send_email
74
+ end
75
+
76
+ def call(name)
77
+ if @repo.user_exist?(name)
78
+ Failure(:user_exists)
79
+ else
80
+ user = yield @repo.add_user(name)
81
+ yield @send_email.(user)
82
+ Success(user)
83
+ end
84
+ end
85
+ end
86
+ ```
87
+ * `Task.failed` is a counterpart of `Task.pure`, accepts an exception and returns a failed task immediately (flash-gordon)
88
+
89
+ [Compare v1.1.0...v1.2.0](https://github.com/dry-rb/dry-monads/compare/v1.1.0...v1.2.0)
90
+
1
91
  # v1.1.0 2018-10-16
2
92
 
3
93
  ## Fixed
@@ -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.2.0"
28
+ spec.required_ruby_version = ">= 2.3.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'
@@ -1,14 +1,54 @@
1
+ require 'dry/monads/registry'
2
+
1
3
  module Dry
2
4
  # Common, idiomatic monads for Ruby
3
5
  #
4
6
  # @api public
5
7
  module Monads
6
8
  def self.included(base)
7
- if const_defined?(:CONSTRUCTORS)
8
- base.include(*CONSTRUCTORS)
9
+ if all_loaded?
10
+ base.include(*constructors)
9
11
  else
10
12
  raise "Load all monads first with require 'dry/monads/all'"
11
13
  end
12
14
  end
15
+
16
+ # Build a module with cherry-picked monads.
17
+ # It saves a bit of typing when you add multiple
18
+ # monads to one class. Not loaded monads get loaded automatically.
19
+ #
20
+ # @example
21
+ # require 'dry/monads'
22
+ #
23
+ # class CreateUser
24
+ # include Dry::Monads[:result, :do]
25
+ #
26
+ # def initialize(repo, send_email)
27
+ # @repo = repo
28
+ # @send_email = send_email
29
+ # end
30
+ #
31
+ # def call(name)
32
+ # if @repo.user_exist?(name)
33
+ # Failure(:user_exists)
34
+ # else
35
+ # user = yield @repo.add_user(name)
36
+ # yield @send_email.(user)
37
+ # Success(user)
38
+ # end
39
+ # end
40
+ # end
41
+ #
42
+ # @param [Array<Symbol>] monads
43
+ # @return [Module]
44
+ # @api public
45
+ def self.[](*monads)
46
+ monads.sort!
47
+ @mixins.fetch_or_store(monads.hash) do
48
+ monads.each { |m| load_monad(m) }
49
+ mixins = monads.map { |m| registry.fetch(m) }
50
+ Module.new { include(*mixins) }.freeze
51
+ end
52
+ end
13
53
  end
14
54
  end
@@ -1,26 +1,9 @@
1
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'
2
+ require 'dry/monads/registry'
11
3
 
12
4
  module Dry
13
5
  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)
6
+ known_monads.each { |m| load_monad(m) }
7
+ extend(*constructors)
25
8
  end
26
9
  end
@@ -77,6 +77,11 @@ module Dry
77
77
 
78
78
  base.prepend(wrappers[base])
79
79
  end
80
+
81
+ def included(base)
82
+ super
83
+ Dry::Monads::Do::All.included(base)
84
+ end
80
85
  end
81
86
  end
82
87
 
@@ -101,5 +106,8 @@ module Dry
101
106
  end
102
107
  end
103
108
  end
109
+
110
+ require 'dry/monads/registry'
111
+ register_mixin(:do, Do::All)
104
112
  end
105
113
  end
@@ -78,6 +78,7 @@ module Dry
78
78
  end
79
79
  end
80
80
 
81
- extend Lazy::Mixin::Constructors
81
+ require 'dry/monads/registry'
82
+ register_mixin(:lazy, Lazy::Mixin)
82
83
  end
83
84
  end
@@ -308,6 +308,39 @@ module Dry
308
308
  self
309
309
  end
310
310
 
311
+ # Iterates over the list and collects Some values.
312
+ #
313
+ # @example with block syntax
314
+ # n = 20
315
+ # List[10, 5, 0].collect do |divisor|
316
+ # if divisor.zero?
317
+ # None()
318
+ # else
319
+ # Some(n / divisor)
320
+ # end
321
+ # end
322
+ # # => List[4, 2]
323
+ #
324
+ # @example without block
325
+ # List[Some(5), None(), Some(3)].collect.map { |x| x * 2 }
326
+ # # => [10, 6]
327
+ #
328
+ # @return [List]
329
+ def collect
330
+ if block_given?
331
+ collected = value.each_with_object([]) do |x, ys|
332
+ y = yield(x)
333
+ ys << y.value! if y.some?
334
+ end
335
+
336
+ List.new(collected)
337
+ else
338
+ Enumerator.new do |g|
339
+ value.each { |x| g << x.value! if x.some? }
340
+ end
341
+ end
342
+ end
343
+
311
344
  private
312
345
 
313
346
  def coerce(other)
@@ -376,6 +409,9 @@ module Dry
376
409
  end
377
410
  end
378
411
  end
412
+
413
+ require 'dry/monads/registry'
414
+ register_mixin(:list, List::Mixin)
379
415
  end
380
416
  end
381
417
 
@@ -296,5 +296,8 @@ module Dry
296
296
  end
297
297
  end
298
298
  end
299
+
300
+ require 'dry/monads/registry'
301
+ register_mixin(:maybe, Maybe::Mixin)
299
302
  end
300
303
  end
@@ -0,0 +1,70 @@
1
+ require 'concurrent/map'
2
+
3
+ module Dry
4
+ # Common, idiomatic monads for Ruby
5
+ #
6
+ # @api private
7
+ module Monads
8
+ @registry = {}
9
+ @constructors = nil
10
+ @paths = {
11
+ do: 'dry/monads/do/all',
12
+ lazy: 'dry/monads/lazy',
13
+ list: 'dry/monads/list',
14
+ maybe: 'dry/monads/maybe',
15
+ task: 'dry/monads/task',
16
+ try: 'dry/monads/try',
17
+ validated: 'dry/monads/validated',
18
+ result: [
19
+ 'dry/monads/result',
20
+ 'dry/monads/result/fixed'
21
+ ]
22
+ }.freeze
23
+ @mixins = Concurrent::Map.new
24
+
25
+ class << self
26
+ private
27
+
28
+ attr_reader :registry
29
+
30
+ def registry=(registry)
31
+ @constructors = nil
32
+ @registry = registry.dup.freeze
33
+ end
34
+ protected :registry=
35
+
36
+ # @private
37
+ def register_mixin(name, mod)
38
+ if registry.key?(name)
39
+ raise ArgumentError, "#{ name.inspect } is already registered"
40
+ end
41
+ self.registry = registry.merge(name => mod)
42
+ end
43
+
44
+ # @private
45
+ def known_monads
46
+ @paths.keys
47
+ end
48
+
49
+ # @private
50
+ def load_monad(name)
51
+ path = @paths.fetch(name) {
52
+ raise ArgumentError, "#{ name.inspect } is not a known monad"
53
+ }
54
+ Array(path).each { |p| require p }
55
+ end
56
+
57
+ # @private
58
+ def constructors
59
+ @constructors ||= registry.values.map { |m|
60
+ m::Constructors if m.const_defined?(:Constructors)
61
+ }.compact
62
+ end
63
+
64
+ # @private
65
+ def all_loaded?
66
+ registry.size == @paths.size
67
+ end
68
+ end
69
+ end
70
+ end
@@ -124,6 +124,8 @@ module Dry
124
124
  include RightBiased::Left
125
125
  include Dry::Equalizer(:failure)
126
126
 
127
+ singleton_class.send(:alias_method, :call, :new)
128
+
127
129
  # Returns a constructor proc
128
130
  #
129
131
  # @return [Proc]
@@ -365,5 +367,8 @@ module Dry
365
367
  end
366
368
  end
367
369
  end
370
+
371
+ require 'dry/monads/registry'
372
+ register_mixin(:result, Result::Mixin)
368
373
  end
369
374
  end
@@ -21,6 +21,7 @@ module Dry
21
21
  def m.to_proc
22
22
  @to_proc ||= method(:new).to_proc
23
23
  end
24
+ m.singleton_class.send(:alias_method, :call, :new)
24
25
  end
25
26
 
26
27
  # Unwraps the underlying value
@@ -144,6 +145,47 @@ module Dry
144
145
  fmap { Unit }
145
146
  end
146
147
 
148
+ # Removes one level of monad structure by joining two values.
149
+ #
150
+ # @example
151
+ # include Dry::Monads::Result::Mixin
152
+ # Success(Success(5)).flatten # => Success(5)
153
+ # Success(Failure(:not_a_number)).flatten # => Failure(:not_a_number)
154
+ # Failure(:not_a_number).flatten # => Failure(:not_a_number)
155
+ #
156
+ # @return [RightBiased::Right,RightBiased::Left]
157
+ def flatten
158
+ bind(&:itself)
159
+ end
160
+
161
+ # Combines the wrapped value with another monadic value.
162
+ # If both values are right-sided, yields a block and passes a tuple
163
+ # of values there. If no block given, returns a tuple of values wrapped with
164
+ # a monadic structure.
165
+ #
166
+ # @example
167
+ # include Dry::Monads::Result::Mixin
168
+ #
169
+ # Success(3).and(Success(5)) # => Success([3, 5])
170
+ # Success(3).and(Failure(:not_a_number)) # => Failure(:not_a_number)
171
+ # Failure(:not_a_number).and(Success(5)) # => Failure(:not_a_number)
172
+ # Success(3).and(Success(5)) { |a, b| a + b } # => Success(8)
173
+ #
174
+ # @param mb [RightBiased::Left,RightBiased::Right]
175
+ #
176
+ # @return [RightBiased::Left,RightBiased::Right]
177
+ def and(mb)
178
+ bind do |a|
179
+ mb.fmap do |b|
180
+ if block_given?
181
+ yield([a, b])
182
+ else
183
+ [a, b]
184
+ end
185
+ end
186
+ end
187
+ end
188
+
147
189
  private
148
190
 
149
191
  # @api private
@@ -243,7 +285,23 @@ module Dry
243
285
  #
244
286
  # @return [RightBiased::Left]
245
287
  def discard
246
- fmap { Unit }
288
+ self
289
+ end
290
+
291
+ # Returns self back. It exists to keep the interface
292
+ # identical to that of {RightBiased::Right}.
293
+ #
294
+ # @return [RightBiased::Left]
295
+ def flatten
296
+ self
297
+ end
298
+
299
+ # Returns self back. It exists to keep the interface
300
+ # identical to that of {RightBiased::Right}.
301
+ #
302
+ # @return [RightBiased::Left]
303
+ def and(_)
304
+ self
247
305
  end
248
306
  end
249
307
  end
@@ -54,7 +54,7 @@ module Dry
54
54
  new(Promise.execute(executor: executor, &block))
55
55
  end
56
56
 
57
- # Returns a complete task from the given value
57
+ # Returns a completed task from the given value
58
58
  #
59
59
  # @overload pure(value)
60
60
  # @param value [Object]
@@ -68,6 +68,14 @@ module Dry
68
68
  v = Undefined.default(value, block)
69
69
  new(Promise.fulfill(v))
70
70
  end
71
+
72
+ # Returns a failed task from the given exception
73
+ #
74
+ # @param exc [Exception]
75
+ # @return [Task]
76
+ def failed(exc)
77
+ new(Promise.reject(exc))
78
+ end
71
79
  end
72
80
 
73
81
  include ConversionStubs[:to_maybe, :to_result]
@@ -299,6 +307,7 @@ module Dry
299
307
  end
300
308
  end
301
309
 
302
- extend Task::Mixin::Constructors
310
+ require 'dry/monads/registry'
311
+ register_mixin(:task, Task::Mixin)
303
312
  end
304
313
  end
@@ -168,6 +168,8 @@ module Dry
168
168
  include Dry::Equalizer(:exception)
169
169
  include RightBiased::Left
170
170
 
171
+ singleton_class.send(:alias_method, :call, :new)
172
+
171
173
  # @param exception [Exception]
172
174
  def initialize(exception)
173
175
  @exception = exception
@@ -273,6 +275,7 @@ module Dry
273
275
  end
274
276
  end
275
277
 
276
- extend Try::Mixin::Constructors
278
+ require 'dry/monads/registry'
279
+ register_mixin(:try, Try::Mixin)
277
280
  end
278
281
  end
@@ -296,5 +296,8 @@ module Dry
296
296
  end
297
297
  end
298
298
  end
299
+
300
+ require 'dry/monads/registry'
301
+ register_mixin(:validated, Validated::Mixin)
299
302
  end
300
303
  end
@@ -1,6 +1,6 @@
1
1
  module Dry
2
2
  module Monads
3
3
  # @private
4
- VERSION = '1.1.0'.freeze
4
+ VERSION = '1.2.0'.freeze
5
5
  end
6
6
  end
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.1.0
4
+ version: 1.2.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: 2018-10-16 00:00:00.000000000 Z
11
+ date: 2019-01-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: dry-equalizer
@@ -147,6 +147,7 @@ files:
147
147
  - lib/dry/monads/lazy.rb
148
148
  - lib/dry/monads/list.rb
149
149
  - lib/dry/monads/maybe.rb
150
+ - lib/dry/monads/registry.rb
150
151
  - lib/dry/monads/result.rb
151
152
  - lib/dry/monads/result/fixed.rb
152
153
  - lib/dry/monads/right_biased.rb
@@ -173,15 +174,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
173
174
  requirements:
174
175
  - - ">="
175
176
  - !ruby/object:Gem::Version
176
- version: 2.2.0
177
+ version: 2.3.0
177
178
  required_rubygems_version: !ruby/object:Gem::Requirement
178
179
  requirements:
179
180
  - - ">="
180
181
  - !ruby/object:Gem::Version
181
182
  version: '0'
182
183
  requirements: []
183
- rubyforge_project:
184
- rubygems_version: 2.7.3
184
+ rubygems_version: 3.0.1
185
185
  signing_key:
186
186
  specification_version: 4
187
187
  summary: Common monads for Ruby.