dry-monads 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
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.