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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ae0ce757dc3f6ec60e8fb1d3e111535fe3e32d40956c5e38cdbef583805b5f6a
4
- data.tar.gz: 83962c10f05f0a53d37a34c77c4197a799a8a78f7a6de767999a6eebd0f30571
3
+ metadata.gz: 3b5983b622b49edc1008fc8717adf6f4150f011c55ca094879f6788cfed657f2
4
+ data.tar.gz: 8e01a982d197dbc26e82df01c804dba93e8aa327aeb9cb248a07eeda7a7c636c
5
5
  SHA512:
6
- metadata.gz: 1754fea24d90657ea3947de3c1b4206d22c977ec30546377ccb82c166a898ca8f4dc0b6b21dd384ceea78a706c29c9570951222f48e6a40b661964b1f43ffef2
7
- data.tar.gz: 7667bcb8d8a009f68055c9cade624e7276a51749f42fdbfacc8e47b13763e10f57e7e34b00878017498f100f9eae55de72e10b9d8ffcad8d952043311aea74ed
6
+ metadata.gz: f81e00f7455a03b12996724fd35404d81dff4cbc934e1868a2329c26cbaae5cb7f5edcde6c9766f754844e8ce0b839d5ca10781e0c1c08702020e938956703b3
7
+ data.tar.gz: 4ea9d2ffa5988996517ba096ef8985331a213fadbdb6d81b2597012a0d7ada3da3716422ba155accd0a9932bf5802a905a3b19162526c5f2a501f45e0b46537e
@@ -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 rake spec
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
- - '[ -d coverage ] && bundle exec codeclimate-test-reporter'
11
+ - "[ -d coverage ] && bundle exec codeclimate-test-reporter"
13
12
  rvm:
14
- - 2.3.8
15
- - 2.4.5
16
- - 2.5.3
17
- - 2.6.0
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 # default: 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 # options: [always|never|change] default: always
38
- on_failure: always # options: [always|never|change] default: always
39
- on_start: false # default: 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
@@ -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
@@ -5,6 +5,7 @@ gemspec
5
5
  group :test do
6
6
  gem 'codeclimate-test-reporter', require: false
7
7
  gem 'simplecov', require: false
8
+ gem 'warning'
8
9
  end
9
10
 
10
11
  group :tools do
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.org/dry-rb/dry-monads
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 Gitter chat](https://badges.gitter.im/Join%20Chat.svg)][gitter]
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
- * [Documentation](http://dry-rb.org/gems/dry-monads)
40
+ - [Documentation](http://dry-rb.org/gems/dry-monads)
41
41
 
42
42
  ## Development
43
43
 
@@ -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.3.0"
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'
@@ -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
@@ -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
- # @private
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
- # Generates a module that passes a block to methods
22
- # that either unwraps a single-valued monadic value or halts
23
- # the execution.
24
- #
25
- # @example A complete example
26
- #
27
- # class CreateUser
28
- # include Dry::Monads::Result::Mixin
29
- # include Dry::Monads::Try::Mixin
30
- # include Dry::Monads::Do.for(:call)
31
- #
32
- # attr_reader :user_repo
33
- #
34
- # def initialize(:user_repo)
35
- # @user_repo = user_repo
36
- # end
37
- #
38
- # def call(params)
39
- # json = yield parse_json(params)
40
- # hash = yield validate(json)
41
- #
42
- # user_repo.transaction do
43
- # user = yield create_user(hash[:user])
44
- # yield create_profile(user, hash[:profile])
45
- # end
46
- #
47
- # Success(user)
48
- # end
49
- #
50
- # private
51
- #
52
- # def parse_json(params)
53
- # Try(JSON::ParserError) {
54
- # JSON.parse(params)
55
- # }.to_result
56
- # end
57
- #
58
- # def validate(json)
59
- # UserSchema.(json).to_monad
60
- # end
61
- #
62
- # def create_user(user_data)
63
- # Try(Sequel::Error) {
64
- # user_repo.create(user_data)
65
- # }.to_result
66
- # end
67
- #
68
- # def create_profile(user, profile_data)
69
- # Try(Sequel::Error) {
70
- # user_repo.create_profile(user, profile_data)
71
- # }.to_result
72
- # end
73
- # end
74
- #
75
- # @param [Array<Symbol>] methods
76
- # @return [Module]
77
- def self.for(*methods)
78
- mod = Module.new do
79
- methods.each { |method_name| Do.wrap_method(self, method_name) }
80
- end
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
- Module.new do
83
- singleton_class.send(:define_method, :included) do |base|
84
- base.prepend(mod)
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
- # @api private
90
- def self.included(base)
91
- super
93
+ # @api private
94
+ def included(base)
95
+ super
92
96
 
93
- # Actually mixes in Do::All
94
- require 'dry/monads/do/all'
95
- base.include All
96
- end
97
-
98
- protected
97
+ # Actually mixes in Do::All
98
+ require 'dry/monads/do/all'
99
+ base.include All
100
+ end
99
101
 
100
- # @private
101
- def self.halt(result)
102
- raise Halt.new(result)
103
- end
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
- # @private
106
- def self.coerce_to_monad(monads)
107
- return monads if monads.size != 1
115
+ # @api private
116
+ def coerce_to_monad(monads)
117
+ return monads if monads.size != 1
108
118
 
109
- first = monads[0]
119
+ first = monads[0]
110
120
 
111
- case first
112
- when Array
113
- [List.coerce(first).traverse]
114
- when List
115
- [first.traverse]
116
- else
117
- monads
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
- # @private
122
- def self.wrap_method(target, method_name)
123
- target.module_eval(<<-RUBY, __FILE__, __LINE__ + 1)
124
- def #{ method_name }(*)
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
@@ -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
+
@@ -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
@@ -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)
@@ -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
- self.class.coerce(bind(*args, &block))
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
- "Some(#{ @value.inspect })"
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
@@ -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
- "Success(#{ @value.inspect })"
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
- "Failure(#{ @value.inspect })"
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
  #
@@ -24,7 +24,8 @@ module Dry::Monads
24
24
  end
25
25
  end
26
26
 
27
- # @api private
27
+ private
28
+
28
29
  def included(base)
29
30
  super
30
31
 
@@ -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
@@ -128,14 +128,18 @@ module Dry
128
128
  def to_s
129
129
  state = case promise.state
130
130
  when :fulfilled
131
- "value=#{ value!.inspect }"
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(#{ state })"
142
+ "Task(#{state})"
139
143
  end
140
144
  alias_method :inspect, :to_s
141
145
 
@@ -20,7 +20,7 @@ module Dry
20
20
  attr_reader :exception
21
21
 
22
22
  class << self
23
- extend Dry::Core::Deprecations[:'dry-monads']
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
- # @private
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
- "Try::Value(#{ @value.inspect })"
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? ? Try::DEFAULT_EXCEPTIONS : exceptions.flatten
242
+ catchable = exceptions.empty? ? DEFAULT_EXCEPTIONS : exceptions.flatten
238
243
  Try.run(catchable, f)
239
244
  end
240
245
  end
@@ -120,7 +120,11 @@ module Dry
120
120
 
121
121
  # @return [String]
122
122
  def inspect
123
- "Valid(#{ value!.inspect })"
123
+ if Unit.equal?(@value)
124
+ "Valid()"
125
+ else
126
+ "Valid(#{@value.inspect})"
127
+ end
124
128
  end
125
129
  alias_method :to_s, :inspect
126
130
 
@@ -1,6 +1,6 @@
1
1
  module Dry
2
2
  module Monads
3
- # @private
4
- VERSION = '1.2.0'.freeze
3
+ # Gem version
4
+ VERSION = '1.3.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.2.0
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-01-15 00:00:00.000000000 Z
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.3.0
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.1
185
+ rubygems_version: 3.0.3
185
186
  signing_key:
186
187
  specification_version: 4
187
188
  summary: Common monads for Ruby.