dry-monads 1.3.2 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +157 -73
  3. data/LICENSE +1 -1
  4. data/README.md +18 -38
  5. data/dry-monads.gemspec +32 -30
  6. data/lib/dry-monads.rb +3 -1
  7. data/lib/dry/monads.rb +4 -2
  8. data/lib/dry/monads/all.rb +4 -2
  9. data/lib/dry/monads/constants.rb +1 -1
  10. data/lib/dry/monads/conversion_stubs.rb +2 -0
  11. data/lib/dry/monads/curry.rb +2 -0
  12. data/lib/dry/monads/do.rb +55 -17
  13. data/lib/dry/monads/do/all.rb +39 -17
  14. data/lib/dry/monads/do/mixin.rb +2 -0
  15. data/lib/dry/monads/either.rb +9 -7
  16. data/lib/dry/monads/errors.rb +8 -3
  17. data/lib/dry/monads/lazy.rb +19 -6
  18. data/lib/dry/monads/list.rb +31 -30
  19. data/lib/dry/monads/maybe.rb +90 -19
  20. data/lib/dry/monads/registry.rb +15 -12
  21. data/lib/dry/monads/result.rb +42 -15
  22. data/lib/dry/monads/result/fixed.rb +35 -24
  23. data/lib/dry/monads/right_biased.rb +45 -24
  24. data/lib/dry/monads/task.rb +25 -22
  25. data/lib/dry/monads/transformer.rb +4 -1
  26. data/lib/dry/monads/traverse.rb +9 -1
  27. data/lib/dry/monads/try.rb +51 -13
  28. data/lib/dry/monads/unit.rb +6 -2
  29. data/lib/dry/monads/validated.rb +27 -20
  30. data/lib/dry/monads/version.rb +3 -1
  31. data/lib/json/add/dry/monads/maybe.rb +4 -3
  32. metadata +27 -75
  33. data/.codeclimate.yml +0 -12
  34. data/.github/ISSUE_TEMPLATE/----please-don-t-ask-for-support-via-issues.md +0 -10
  35. data/.github/ISSUE_TEMPLATE/---bug-report.md +0 -34
  36. data/.github/ISSUE_TEMPLATE/---feature-request.md +0 -18
  37. data/.github/workflows/ci.yml +0 -74
  38. data/.github/workflows/docsite.yml +0 -34
  39. data/.github/workflows/sync_configs.yml +0 -34
  40. data/.gitignore +0 -10
  41. data/.rspec +0 -4
  42. data/.rubocop.yml +0 -89
  43. data/.yardopts +0 -4
  44. data/CODE_OF_CONDUCT.md +0 -13
  45. data/CONTRIBUTING.md +0 -29
  46. data/Gemfile +0 -23
  47. data/Rakefile +0 -6
  48. data/bin/console +0 -16
  49. data/bin/setup +0 -7
  50. data/docsite/source/case-equality.html.md +0 -42
  51. data/docsite/source/do-notation.html.md +0 -207
  52. data/docsite/source/getting-started.html.md +0 -142
  53. data/docsite/source/index.html.md +0 -179
  54. data/docsite/source/list.html.md +0 -87
  55. data/docsite/source/maybe.html.md +0 -146
  56. data/docsite/source/pattern-matching.html.md +0 -68
  57. data/docsite/source/result.html.md +0 -190
  58. data/docsite/source/task.html.md +0 -126
  59. data/docsite/source/tracing-failures.html.md +0 -32
  60. data/docsite/source/try.html.md +0 -76
  61. data/docsite/source/unit.html.md +0 -36
  62. data/docsite/source/validated.html.md +0 -88
  63. data/log/.gitkeep +0 -0
@@ -1,126 +0,0 @@
1
- ---
2
- title: Task
3
- layout: gem-single
4
- name: dry-monads
5
- ---
6
-
7
- `Task` represents an asynchronous computation. It is similar to the `IO` type in a sense it can be used to wrap side-effectful actions. `Task`s are usually run on a thread pool but also can be executed immediately on the current thread. Internally, `Task` uses `Promise` from the [`concurrent-ruby`](https://github.com/ruby-concurrency/concurrent-ruby) gem, basically it's a thin wrapper with a monadic interface which makes it easily composable with other monads.
8
-
9
- ### `Task::Mixin`
10
-
11
- Basic usage.
12
-
13
- ```ruby
14
- require 'dry/monads'
15
-
16
- class PullUsersWithPosts
17
- include Dry::Monads[:task]
18
-
19
- def call
20
- # Start two tasks running concurrently
21
- users = Task { fetch_users }
22
- posts = Task { fetch_posts }
23
-
24
- # Combine two tasks
25
- users.bind { |us| posts.fmap { |ps| [us, ps] } }
26
- end
27
-
28
- def fetch_users
29
- sleep 3
30
- [{ id: 1, name: 'John' }, { id: 2, name: 'Jane' }]
31
- end
32
-
33
- def fetch_posts
34
- sleep 2
35
- [
36
- { id: 1, user_id: 1, name: 'Hello from John' },
37
- { id: 2, user_id: 2, name: 'Hello from Jane' },
38
- ]
39
- end
40
- end
41
-
42
- # PullUsersWithPosts instance
43
- pull = PullUsersWithPosts.new
44
-
45
- # Spin up two tasks
46
- task = pull.call
47
-
48
- task.fmap do |users, posts|
49
- puts "Users: #{ users.inspect }"
50
- puts "Posts: #{ posts.inspect }"
51
- end
52
-
53
- puts "----" # this will be printed before the lines above
54
- ```
55
-
56
- ### Executors
57
-
58
- Tasks are performed by executors, there are three executors predefined by `concurrent-ruby` identified by symbols:
59
-
60
- - `:fast` – for fast asynchronous tasks, uses a thread pool
61
- - `:io` – for long IO-bound tasks, uses a thread pool, different from `:fast`
62
- - `:immediate` – runs tasks immediately, on the current thread. Can be used in tests or for other purposes
63
-
64
- You can create your own executors, check out the [docs](http://ruby-concurrency.github.io/concurrent-ruby/master/Concurrent.html) for more on this.
65
-
66
- The following examples use the Ruby 2.5+ syntax which allows passing a block to `.[]`.
67
-
68
- ```ruby
69
- Task[:io] { do_http_request }
70
-
71
- Task[:fast] { cpu_intensive_computation }
72
-
73
- Task[:immediate] { unsafe_io_operation }
74
-
75
- # You can pass an executor object
76
- Task[my_executor] { ... }
77
- ```
78
-
79
- ### Exception handling
80
-
81
- All exceptions happening in `Task` are captured, even if you're using the `:immediate` executor, they won't be re-raised.
82
-
83
- ```ruby
84
- io_fail = Task[:io] { 1/0 }
85
- io_fail # => Task(error=#<ZeroDivisionError: divided by 0>)
86
-
87
- immediate_fail = Task[:immediate] { 1/0 }
88
- immediate_fail # => Task(error=#<ZeroDivisionError: divided by 0>)
89
- ```
90
-
91
- You can process failures with `or` and `or_fmap`:
92
-
93
- ```ruby
94
- Task[:immediate] { 1/0 }.or { M::Task[:immediate] { 0 } } # => Task(value=0)
95
- Task[:immediate] { 1/0 }.or_fmap { 0 } # => Task(value=0)
96
- ```
97
-
98
- ### Extracting result
99
-
100
- Getting the result of a task is an unsafe operation, it blocks the current thread until the task is finished, then returns the value or raises an exception if the evaluation wasn't sucessful. It effectively cancels all niceties of tasks so you shouldn't use it in production code.
101
-
102
- ```ruby
103
- Task { 0 }.value! # => 0
104
- Task { 1/0 }.value! # => ZeroDivisionError: divided by 0
105
- ```
106
-
107
- You can wait for a task to complete, the `wait` method accepts an optional timeout. `.wait` returns the task back, without unwrapping the result so it's a blocking yet safe operation:
108
-
109
- ```ruby
110
- Task[:io] { 2 }.wait(1) # => Task(value=2)
111
- Task[:io] { sleep 2; 2 }.wait(1) # => Task(?)
112
-
113
- # (?) denotes an unfinished computation
114
- ```
115
-
116
- ### Conversions
117
-
118
- Tasks can be converted to other monads but keep in mind that all conversions block the current thread:
119
-
120
- ```ruby
121
- Task[:io] { 2 }.to_result # => Success(2)
122
- Task[:io] { 1/0 }.to_result # => Failure(#<ZeroDivisionError: divided by 0>)
123
-
124
- Task[:io] { 2 }.to_maybe # => Some(2)
125
- Task[:io] { 1/0 }.to_maybe # => None
126
- ```
@@ -1,32 +0,0 @@
1
- ---
2
- title: Tracing failures
3
- layout: gem-single
4
- name: dry-monads
5
- ---
6
-
7
- "Left" values of right-biased monads like `Maybe` and `Result` (as in, `None()` and `Failure()`) ignore blocks passed to `fmap` and `bind`. Because of this, these values travel across the application without any modification. If the place where a `Failure` was constructed is burried somewhere deep in the app or library code it may be pretty hard to find out where exactly the error occurred.
8
-
9
- This is a noticable downside compared to "good" old exceptions. To address it, every `Failure(...)` and `None()` value tracks the line where it was created:
10
-
11
- ```ruby
12
- # create_user.rb
13
- require 'dry/monads'
14
-
15
- class CreateUser
16
- include Dry::Monads[:result]
17
-
18
- def call
19
- Failure(:no_luck)
20
- end
21
- end
22
- ```
23
-
24
- ```ruby
25
- require 'create_user'
26
-
27
- create_user = CreateUser.new
28
- create_user.() # => Failure(:no_luck)
29
- create_user.().trace # => .../create_user.rb:8:in `call'
30
- ```
31
-
32
- Note that the trace stores only one line of the stack so it shouldn't ever be a performance issue.
@@ -1,76 +0,0 @@
1
- ---
2
- title: Try
3
- layout: gem-single
4
- name: dry-monads
5
- ---
6
-
7
- Rescues a block from an exception. The `Try` monad is useful when you want to wrap some code that can raise exceptions of certain types. A common example is making an HTTP request or querying a database.
8
-
9
- ```ruby
10
- require 'dry/monads'
11
-
12
- class ExceptionalLand
13
- include Dry::Monads[:try]
14
-
15
-
16
- def call
17
- res = Try { 10 / 2 }
18
- res.value! if res.value?
19
- # => 5
20
-
21
- res = Try { 10 / 0 }
22
- res.exception if res.error?
23
- # => #<ZeroDivisionError: divided by 0>
24
-
25
- # By default Try catches all exceptions inherited from StandardError.
26
- # However you can catch only certain exceptions like this
27
- Try[NoMethodError, NotImplementedError] { 10 / 0 }
28
- # => raised ZeroDivisionError: divided by 0 exception
29
- end
30
- end
31
- ```
32
-
33
- It is better if you pass a list of expected exceptions which you are sure you can process. Catching exceptions of all types is considered bad practice.
34
-
35
- The `Try` monad consists of two types: `Value` and `Error`. The first is returned when code did not raise an error and the second is returned when the error was captured.
36
-
37
- ### `bind`
38
-
39
- Allows you to chain blocks that can raise exceptions.
40
-
41
- ```ruby
42
- Try[NetworkError, DBError] { grap_user_by_making_request }.bind { |user| user_repo.save(user) }
43
-
44
- # Possible outcomes:
45
- # => Value(persisted_user)
46
- # => Error(NetworkError: request timeout)
47
- # => Error(DBError: unique constraint violated)
48
- ```
49
-
50
- ### `fmap`
51
-
52
- Works exactly the same way as `Result#fmap` does.
53
-
54
- ```ruby
55
- require 'dry/monads'
56
-
57
- class ExceptionalLand
58
- include Dry::Monads[:try]
59
-
60
- def call
61
- Try { 10 / 2 }.fmap { |x| x * 3 }
62
- # => 15
63
-
64
- Try[ZeroDivisionError] { 10 / 0 }.fmap { |x| x * 3 }
65
- # => Failure(ZeroDivisionError: divided by 0)
66
- end
67
- end
68
- ```
69
-
70
- ### `value!` and `exception`
71
-
72
- Use `value!` for unwrapping a `Success` and `exception` for getting error object from a `Failure`.
73
-
74
- ### `to_result` and `to_maybe`
75
-
76
- `Try`'s `Value` and `Error` can be transformed to `Success` and `Failure` correspondingly by calling `to_result` and to `Some` and `None` by calling `to_maybe`. Keep in mind that by transforming `Try` to `Maybe` you lose the information about an exception so be sure that you've processed the error before doing so.
@@ -1,36 +0,0 @@
1
- ---
2
- title: Unit
3
- layout: gem-single
4
- name: dry-monads
5
- ---
6
-
7
- Some constructors do not require you to pass a value. As a default they use `Unit`, a special singleton value:
8
-
9
- ```ruby
10
- extend Dry::Monads[:result]
11
-
12
- Success().value! # => Unit
13
- ```
14
-
15
- `Unit` doesn't have any special properties or methods, it's similar to `nil` except for it is not i.e. `if Unit` passes.
16
-
17
- `Unit` is usually excluded from the output:
18
-
19
- ```ruby
20
- extend Dry::Monads[:result]
21
-
22
- # Outputs as "Success()" but technically it's "Success(Unit)"
23
- Success()
24
- ```
25
-
26
- ### Discarding values
27
-
28
- When the outcome of an operation is not a caller's concern, call `.discard`, it will map the wrapped value to `Unit`:
29
-
30
- ```ruby
31
- extend Dry::Monads[:result]
32
-
33
- result = create_user # returns Success(#<User...>) or Failure(...)
34
-
35
- result.discard # => Maps Success(#<User ...>) to Success() but lefts Failure(...) intact
36
- ```
@@ -1,88 +0,0 @@
1
- ---
2
- title: Validated
3
- layout: gem-single
4
- name: dry-monads
5
- ---
6
-
7
- Suppose you've got a form to validate. If you are using `Result` combined with `Do` your code might look like this:
8
-
9
- ```ruby
10
- require 'dry/monads'
11
-
12
- class CreateAccount
13
- include Dry::Monads[:result, :do]
14
-
15
- def call(form)
16
- name = yield validate_name(form)
17
- email = yield validate_email(form)
18
- password = yield validate_password(form)
19
-
20
- user = repo.create_user(
21
- name: name,
22
- email: email,
23
- password: password
24
- )
25
-
26
- Success(user)
27
- end
28
-
29
- def validate_name(form)
30
- # Success(name) or Failure(:invalid_name)
31
- end
32
-
33
- def validate_email(form)
34
- # Success(email) or Failure(:invalid_email)
35
- end
36
-
37
- def validate_password(form)
38
- # Success(password) or Failure(:invalid_password)
39
- end
40
- end
41
- ```
42
-
43
- If any of the validation steps fails the user will see an error. The problem is if `name` is not valid the user won't see errors about invalid `email` and `password`, if any. `Validated` circumvents this particular problem.
44
-
45
- `Validated` is actually not a monad but an applicative functor. This means you can't call `bind` on it. Instead, it can accumulate values in combination with `List`:
46
-
47
- ```ruby
48
- require 'dry/monads'
49
-
50
- class CreateAccount
51
- include Dry::Monads[:list, :result, :validated, :do]
52
-
53
- def call(form)
54
- name, email, password = yield List::Validated[
55
- validate_name(form),
56
- validate_email(form),
57
- validate_password(form)
58
- ].traverse.to_result
59
-
60
- user = repo.create_user(
61
- name: name,
62
- email: email,
63
- password: password
64
- )
65
-
66
- Success(user)
67
- end
68
-
69
- def validate_name(form)
70
- # Valid(name) or Invalid(:invalid_name)
71
- end
72
-
73
- def validate_email(form)
74
- # Valid(email) or Invalid(:invalid_email)
75
- end
76
-
77
- def validate_password(form)
78
- # Valid(password) or Invalid(:invalid_password)
79
- end
80
- end
81
- ```
82
-
83
- Here all validations will be processed at once, if any of them fails the result will be converted to a `Failure` wrapping the `List` of errors:
84
-
85
- ```ruby
86
- create_account.(form)
87
- # => Failure(List[:invalid_name, :invalid_email])
88
- ```
data/log/.gitkeep DELETED
File without changes