dry-monads 1.2.0 → 1.3.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: 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.