dry-monads 0.4.0 → 1.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 90f99efaec5f7f88c8f150a6af7d9f3ee2bc2a64
4
- data.tar.gz: 82c6bb698fa47243c44367c14ab182d893c0b59c
2
+ SHA256:
3
+ metadata.gz: e39ab380c859836fb98aad8bc158eeafb73896472cc703254c0f21490c6cde68
4
+ data.tar.gz: 749f9cf55040d0cafc8f8f4e3d8c60a8e2486768223069adff7f0b4c55cc9a43
5
5
  SHA512:
6
- metadata.gz: 559a48263fd284e9c93c1773846b66960653a37ca811b249f5482a1cc518aa83a40e71e7adb5d206c50066293e353317793cb68893935c37f8f12a36654ad854
7
- data.tar.gz: 8c06400c56a9b753d83df82cd011477cc29ba492a3383d1df325e9fa7eaafd56bef653590cde58c4b415994c63817eac1574b12a769267cf2060ab8c0eead8d5
6
+ metadata.gz: 3c66d5ea73e5c781e26acfb1611067a34cc25a9fbbb258a01ad8f5705b33cd569944ae9eab126e18456b4f6eeec0722163e39406e52c332e5224a78b5dae53e9
7
+ data.tar.gz: 73a8d308f628a5ea630cabdaf240f67627e3310ec3cf64a75cec7140b6c5645853cfcf50ecc463a38f1813c28b78a145d7d40ec45998327c7cbedce1177c60c5
data/.travis.yml CHANGED
@@ -5,13 +5,16 @@ cache: bundler
5
5
  bundler_args: --without benchmarks docs
6
6
  script:
7
7
  - bundle exec rake spec
8
+ before_install:
9
+ - gem update --system
8
10
  after_success:
9
11
  - '[ -d coverage ] && bundle exec codeclimate-test-reporter'
10
12
  rvm:
11
- - 2.2.8
12
- - 2.3.5
13
- - 2.4.2
14
- - jruby-9.1.13.0
13
+ - 2.2.9
14
+ - 2.3.6
15
+ - 2.4.3
16
+ - 2.5.0
17
+ - jruby-9.1.15.0
15
18
  - ruby-head
16
19
  env:
17
20
  global:
data/CHANGELOG.md CHANGED
@@ -1,3 +1,143 @@
1
+ # v1.0.0 to-be-released
2
+
3
+ ## Added
4
+
5
+ * `do`-like notation (the idea comes from Haskell of course). This is the biggest and most important addition in the release which greatly increases the ergonomics of using monads in Ruby. Basically, almost everything it does is passing a block to a given method. You call `yield` on monads to extract the values. If any operation fails i.e. no value can be extracted, the whole computation is halted and the failing step becomes a result. With `Do` you don't need to chain monadic values with `fmap/bind` and block, everything can be done on a single level of indentation. Here is a more or less real-life example:
6
+
7
+ ```ruby
8
+ class CreateUser
9
+ include Dry::Monads
10
+ include Dry::Monads::Do.for(:call)
11
+
12
+ attr_reader :user_repo
13
+
14
+ def initialize(:user_repo)
15
+ @user_repo = user_repo
16
+ end
17
+
18
+ def call(params)
19
+ json = yield parse_json(params)
20
+ hash = yield validate(json)
21
+
22
+ user_repo.transaction do
23
+ user = yield create_user(hash[:user])
24
+ yield create_profile(user, hash[:profile])
25
+
26
+ Success(user)
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def parse_json(params)
33
+ Try[JSON::ParserError] {
34
+ JSON.parse(params)
35
+ }.to_result
36
+ end
37
+
38
+ def validate(json)
39
+ UserSchema.(json).to_monad
40
+ end
41
+
42
+ def create_user(user_data)
43
+ Try[Sequel::Error] { user_repo.create(user_data) }.to_result
44
+ end
45
+
46
+ def create_profile(user, profile_data)
47
+ Try[Sequel::Error] {
48
+ user_repo.create_profile(user, profile_data)
49
+ }.to_result
50
+ end
51
+ end
52
+ ```
53
+
54
+ In the code above any `yield` can potentially fail and return the failure reason as a result. In other words, `yield None` acts as `return None`. Internally, `Do` uses exceptions, not `return`, this is somewhat slower but allows to detect failed operations in DB-transactions and roll back the changes which far more useful than an unjustifiable speed boost (flash-gordon)
55
+
56
+ * The `Task` monad based on `Promise` from the [`concurrent-ruby` gem](https://github.com/ruby-concurrency/concurrent-ruby/). `Task` represents an asynchrounos computation which _can be_ (doesn't have to!) run on a seperated thread. `Promise` already offers a good API and implemented in a safe manner so `dry-monads` just adds a monad-compatible interface for it. Out of the box, `concurrent-ruby` has three types of executors for running blocks: `:io`, `:fast`, `:immediate`, check out [the docs](http://ruby-concurrency.github.io/concurrent-ruby/root/Concurrent.html#executor-class_method) for details. You can provide your own executor if needed (flash-gordon)
57
+
58
+ ```ruby
59
+ include Dry::Monads::Task::Mixin
60
+
61
+ def call
62
+ name = Task { get_name_via_http } # runs a request in the background
63
+ email = Task { get_email_via_http } # runs another one request in the background
64
+
65
+ # to_result forces both computations/requests to complete by pausing current thread
66
+ # returns `Result::Success/Result::Failure`
67
+ name.bind { |n| email.fmap { |e| create(e, n) } }.to_result
68
+ end
69
+ ```
70
+
71
+ `Task` works perfectly with `Do`
72
+
73
+ ```ruby
74
+ include Dry::Monads::Do.for(:call)
75
+
76
+ def call
77
+ name, email = yield Task { get_name_via_http }, Task { get_email_via_http }
78
+ Success(create(e, n))
79
+ end
80
+ ```
81
+
82
+ * `Lazy` is a copy of `Task` that isn't run until you ask for the value _for the first time_. It is guaranteed the evaluation is run at most once as opposed to lazy assignment `||=` which isn't synchronized. `Lazy` is run on the same thread asking for the value (flash-gordon)
83
+
84
+ * Automatic type inference with `.typed` for lists was deprecated. Instead, typed list builders were added
85
+
86
+ ```ruby
87
+ list = List::Task[Task { get_name }, Task { get_email }]
88
+ list.traverse # => Task(List['John', 'john@doe.org'])
89
+ ```
90
+
91
+ The code above runs two tasks in parallel and automatically combines their results with `traverse` (flash-gordon)
92
+
93
+ * `Try` got a new call syntax supported in Ruby 2.5+
94
+
95
+ ```ruby
96
+ Try[ArgumentError, TypeError] { unsafe_operation }
97
+ ```
98
+
99
+ Prior to 2.5, it wasn't possible to pass a block to `[]`.
100
+
101
+ * The `Validated` “monad” that represents a result of a validation. Suppose, you want to collect all the errors and return them at once. You can't have it with `Result` because when you `traverse` a `List` of `Result`s it returns the first value and this is the correct behavior from the theoretical point of view. `Validated`, in fact, doesn't have a monad instance but provides a useful variant of applicative which concatenates the errors.
102
+
103
+ ```ruby
104
+ include Dry::Monads
105
+ include Dry::Monads::Do.for(:call)
106
+
107
+ def call(input)
108
+ name, email = yield [
109
+ validate_name(input[:name]),
110
+ validate_email(input[:email])
111
+ ]
112
+
113
+ Success(create(name, email))
114
+ end
115
+
116
+ # can return
117
+ # * Success(User(...))
118
+ # * Invalid(List[:invalid_name])
119
+ # * Invalid(List[:invalid_email])
120
+ # * Invalid(List[:invalid_name, :invalid_email])
121
+ ```
122
+
123
+ In the example above an array of `Validated` values is implicitly casted to `List::Validated`. It's supported because it's useful but don't forget it's all about types and don't mix different types of monads in a single array, the consequences are unclear. You always can be explicit with `List::Validated[validate_name(...), ...]`, choose what you like (flash-gordon).
124
+
125
+ * `Failure`, `None`, and `Invalid` values now store the line where they were created. On of the biggest downside of dealing wtih monadic code is lack of backtraces. If you have a long list of computations and one of them fails how do you know where did it actually happen? Say, you've got `None` and this tells you nothing about _what variable_ was assigned to `None`. It makes sense to use `Result` instead of `Maybe` and use distinct errors everywhere but it doesn't always look good and forces you to think more. TLDR; call `.trace` to get the line where a fail-case was constructed
126
+
127
+ ```ruby
128
+ Failure(:invalid_name).trace # => app/operations/create_user.rb:43
129
+ ```
130
+
131
+ ## Deprecations
132
+
133
+ * `Either`, the former name of `Result`, is now deprecated
134
+
135
+ ## BREAKING CHANGES
136
+
137
+ * `Either#value` and `Maybe#value` were both droped, use `value_or` or `value!` when you :100: sure it's safe
138
+
139
+ [Compare v0.4.0...v1.0.0](https://github.com/dry-rb/dry-monads/compare/v0.4.0...master)
140
+
1
141
  # v0.4.0 2017-11-11
2
142
 
3
143
  ## Changed
@@ -23,7 +163,7 @@
23
163
 
24
164
  ## Deprecated
25
165
 
26
- * Direct accessing `value` on right-biased monads has been deprecated, use the `value!` method instead. `value!` will raise an exception if it is called on a Sailure/None/Error instance (flash-gordon)
166
+ * Direct accessing `value` on right-biased monads has been deprecated, use the `value!` method instead. `value!` will raise an exception if it is called on a Failure/None/Error instance (flash-gordon)
27
167
 
28
168
  [Compare v0.3.1...v0.4.0](https://github.com/dry-rb/dry-monads/compare/v0.3.1...v0.4.0)
29
169
 
data/CONTRIBUTING.md CHANGED
@@ -22,7 +22,7 @@ Other requirements:
22
22
  2) Follow the style conventions of the surrounding code. In most cases, this is standard ruby style.
23
23
  3) Add API documentation if it's a new feature
24
24
  4) Update API documentation if it changes an existing feature
25
- 5) Bonus points for sending a PR to [github.com/dry-rb/dry-rb.org](github.com/dry-rb/dry-rb.org) which updates user documentation and guides
25
+ 5) Bonus points for sending a PR to [github.com/dry-rb/dry-rb.org](https://github.com/dry-rb/dry-rb.org) which updates user documentation and guides
26
26
 
27
27
  # Asking for help
28
28
 
data/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2016 Nikita Shilnikov
1
+ Copyright (c) 2016-2018 Nikita Shilnikov
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
data/README.md CHANGED
@@ -8,8 +8,8 @@
8
8
 
9
9
  [![Gem Version](https://img.shields.io/gem/v/dry-monads.svg)][gem]
10
10
  [![Build Status](https://img.shields.io/travis/dry-rb/dry-monads.svg)][travis]
11
- [![Code Climate](https://img.shields.io/codeclimate/github/dry-rb/dry-monads.svg)][code_climate]
12
- [![Test Coverage](https://img.shields.io/codeclimate/coverage/github/dry-rb/dry-monads.svg)][code_climate]
11
+ [![Code Climate](https://api.codeclimate.com/v1/badges/b0ea4d8023d53b7f0f50/maintainability)][code_climate]
12
+ [![Test Coverage](https://api.codeclimate.com/v1/badges/b0ea4d8023d53b7f0f50/test_coverage)][code_climate]
13
13
  [![API Documentation Coverage](http://inch-ci.org/github/dry-rb/dry-monads.svg)][inch]
14
14
  ![No monkey-patches](https://img.shields.io/badge/monkey--patches-0-brightgreen.svg)
15
15
 
data/dry-monads.gemspec CHANGED
@@ -27,7 +27,8 @@ Gem::Specification.new do |spec|
27
27
  spec.require_paths = ['lib']
28
28
  spec.required_ruby_version = ">= 2.2.0"
29
29
  spec.add_dependency 'dry-equalizer'
30
- spec.add_dependency 'dry-core', '~> 0.3', '>= 0.3.3'
30
+ spec.add_dependency 'dry-core', '~> 0.4', '>= 0.4.4'
31
+ spec.add_dependency 'concurrent-ruby', '~> 1.0'
31
32
 
32
33
  spec.add_development_dependency 'bundler'
33
34
  spec.add_development_dependency 'rake'
data/lib/dry/monads.rb CHANGED
@@ -2,26 +2,47 @@ require 'dry/core/constants'
2
2
  require 'dry/monads/maybe'
3
3
  require 'dry/monads/try'
4
4
  require 'dry/monads/list'
5
+ require 'dry/monads/task'
6
+ require 'dry/monads/lazy'
5
7
  require 'dry/monads/result'
6
8
  require 'dry/monads/result/fixed'
9
+ require 'dry/monads/do'
10
+ require 'dry/monads/validated'
7
11
 
8
12
  module Dry
13
+ # Common, idiomatic monads for Ruby
14
+ #
9
15
  # @api public
10
16
  module Monads
17
+ # @private
11
18
  Undefined = Dry::Core::Constants::Undefined
12
19
 
20
+ # List of monad constructors
13
21
  CONSTRUCTORS = [
14
22
  Maybe::Mixin::Constructors,
15
- Result::Mixin::Constructors
23
+ Result::Mixin::Constructors,
24
+ Validated::Mixin::Constructors,
25
+ Try::Mixin::Constructors,
26
+ Task::Mixin::Constructors,
27
+ Lazy::Mixin::Constructors
16
28
  ].freeze
17
29
 
30
+ # @see Maybe::Some
18
31
  Some = Maybe::Some
32
+ # @see Maybe::None
19
33
  None = Maybe::None
34
+ # @see Result::Success
20
35
  Success = Result::Success
36
+ # @see Result::Failure
21
37
  Failure = Result::Failure
38
+ # @see Validated::Valid
39
+ Valid = Validated::Valid
40
+ # @see Validated::Invalid
41
+ Invalid = Validated::Invalid
22
42
 
23
43
  extend(*CONSTRUCTORS)
24
44
 
45
+ # @private
25
46
  def self.included(base)
26
47
  super
27
48
 
@@ -0,0 +1,19 @@
1
+ module Dry
2
+ module Monads
3
+ # @private
4
+ module Curry
5
+ # @private
6
+ def self.call(value)
7
+ func = value.is_a?(Proc) ? value : value.method(:call)
8
+ seq_args = func.parameters.count { |type, _| type == :req || type == :opt }
9
+ seq_args += 1 if func.parameters.any? { |type, _| type == :keyreq }
10
+
11
+ if seq_args > 1
12
+ func.curry
13
+ else
14
+ func
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,119 @@
1
+ module Dry
2
+ module Monads
3
+ # An implementation of do-notation.
4
+ #
5
+ # @see Do.for
6
+ module Do
7
+ # @private
8
+ class Halt < StandardError
9
+ # @private
10
+ attr_reader :result
11
+
12
+ def initialize(result)
13
+ super()
14
+
15
+ @result = result
16
+ end
17
+ end
18
+
19
+ # Generates a module that passes a block to methods
20
+ # that either unwraps a single-valued monadic value or halts
21
+ # the execution.
22
+ #
23
+ # @example A complete example
24
+ #
25
+ # class CreateUser
26
+ # include Dry::Monads::Result::Mixin
27
+ # include Dry::Monads::Try::Mixin
28
+ # include Dry::Monads::Do.for(:call)
29
+ #
30
+ # attr_reader :user_repo
31
+ #
32
+ # def initialize(:user_repo)
33
+ # @user_repo = user_repo
34
+ # end
35
+ #
36
+ # def call(params)
37
+ # json = yield parse_json(params)
38
+ # hash = yield validate(json)
39
+ #
40
+ # user_repo.transaction do
41
+ # user = yield create_user(hash[:user])
42
+ # yield create_profile(user, hash[:profile])
43
+ # end
44
+ #
45
+ # Success(user)
46
+ # end
47
+ #
48
+ # private
49
+ #
50
+ # def parse_json(params)
51
+ # Try(JSON::ParserError) {
52
+ # JSON.parse(params)
53
+ # }.to_result
54
+ # end
55
+ #
56
+ # def validate(json)
57
+ # UserSchema.(json).to_monad
58
+ # end
59
+ #
60
+ # def create_user(user_data)
61
+ # Try(Sequel::Error) {
62
+ # user_repo.create(user_data)
63
+ # }.to_result
64
+ # end
65
+ #
66
+ # def create_profile(user, profile_data)
67
+ # Try(Sequel::Error) {
68
+ # user_repo.create_profile(user, profile_data)
69
+ # }.to_result
70
+ # end
71
+ # end
72
+ #
73
+ # @param [Array<Symbol>] methods
74
+ # @return [Module]
75
+ def self.for(*methods)
76
+ mod = Module.new do
77
+ methods.each do |method|
78
+ class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
79
+ def #{ method }(*)
80
+ super do |*ms|
81
+ ms = coerce_to_monad(ms)
82
+ unwrapped = ms.map { |m| m.to_monad.or { halt(m) }.value! }
83
+ ms.size == 1 ? unwrapped[0] : unwrapped
84
+ end
85
+ rescue Halt => e
86
+ e.result
87
+ end
88
+ RUBY
89
+ end
90
+ end
91
+
92
+ Module.new do
93
+ singleton_class.send(:define_method, :included) do |base|
94
+ base.prepend(mod)
95
+ end
96
+
97
+ def halt(result)
98
+ raise Halt.new(result)
99
+ end
100
+
101
+ # @private
102
+ def coerce_to_monad(ms)
103
+ return ms if ms.size != 1
104
+
105
+ fst = ms[0]
106
+
107
+ case fst
108
+ when Array, List
109
+ list = fst.is_a?(Array) ? List.coerce(fst) : fst
110
+ [list.traverse]
111
+ else
112
+ ms
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
@@ -1 +1,64 @@
1
+ require 'dry/core/deprecations'
2
+
3
+ Dry::Core::Deprecations.warn('Either monad was renamed to Result', tag: :'dry-monads')
4
+
1
5
  require 'dry/monads/result'
6
+
7
+ module Dry
8
+ module Monads
9
+ Either = Result
10
+ deprecate_constant :Either
11
+
12
+ class Result
13
+ extend Dry::Core::Deprecations[:'dry-monads']
14
+
15
+ deprecate :to_either, :to_result
16
+
17
+ Right = Success
18
+ Left = Failure
19
+
20
+ deprecate_constant :Right
21
+ deprecate_constant :Left
22
+
23
+ module Mixin
24
+ module Constructors
25
+ extend Dry::Core::Deprecations[:'dry-monads']
26
+
27
+ Right = Success
28
+ Left = Failure
29
+ deprecate_constant :Right
30
+ deprecate_constant :Left
31
+
32
+ deprecate :Right, :Success
33
+ deprecate :Left, :Failure
34
+ end
35
+ end
36
+
37
+ class Success
38
+ deprecate :left?, :failure?
39
+ deprecate :right?, :success?
40
+ end
41
+
42
+ class Failure
43
+ deprecate :left?, :failure?
44
+ deprecate :right?, :success?
45
+
46
+ deprecate :left, :failure
47
+ end
48
+ end
49
+
50
+ class Try
51
+ class Value
52
+ extend Dry::Core::Deprecations[:'dry-monads']
53
+
54
+ deprecate :to_either, :to_result
55
+ end
56
+
57
+ class Error
58
+ extend Dry::Core::Deprecations[:'dry-monads']
59
+
60
+ deprecate :to_either, :to_result
61
+ end
62
+ end
63
+ end
64
+ end