result-monad 0.1.4 → 0.2.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 +4 -4
- data/README.md +0 -106
- data/lib/result-monad.rb +17 -143
- data/lib/result-monad/error.rb +5 -19
- data/lib/result-monad/ok.rb +6 -11
- data/lib/result-monad/result_error.rb +2 -0
- data/lib/result-monad/result_methods.rb +79 -0
- data/lib/result-monad/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a15b6b9f028996e96deaa63cc18edb7e314e1043
|
4
|
+
data.tar.gz: 7a45714caaf085a3c3d11ad4b08525758dcf2daa
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d80f7dbfaedf0fb7ebf4bf1d483757d792d455f572f4bb51a0f29a9b1bead9924a16f2178e66e69fef24ab2f0647716cee783c6990189331dbbbf2083ab7219b
|
7
|
+
data.tar.gz: a10d6be8ae576889ea88c4190f7784974de3c372c61b20eb49fdc9289e2b2e977cb161e952f1f8e0c9a06ab9b21b52cc1bf203c1378dc5e16433fe397b432875
|
data/README.md
CHANGED
@@ -87,112 +87,6 @@ x.map {|i| i / 10}
|
|
87
87
|
#=>Result<Error(divided by 0)>
|
88
88
|
```
|
89
89
|
|
90
|
-
#### Performing actions in sequence
|
91
|
-
|
92
|
-
```ruby
|
93
|
-
x = Ok(5)
|
94
|
-
y = Error(99)
|
95
|
-
z = Ok(25)
|
96
|
-
|
97
|
-
# In this case, imagine x and z as successful actions, whereas y is an unsuccessful action.
|
98
|
-
|
99
|
-
x & y & z #=> Result<Error(99)>
|
100
|
-
|
101
|
-
```
|
102
|
-
|
103
|
-
#### Real World Scenario
|
104
|
-
|
105
|
-
Lets say we are using an external library to read data from a file over FTP,
|
106
|
-
write it to a local file, and then delete the file on the FTP server. Some things to note:
|
107
|
-
|
108
|
-
* We use overloaded operator & to chain on Ok
|
109
|
-
* Every call to any outside library, even standard library functions are either in a `map` block which
|
110
|
-
gets Captured or they are themselves Captured
|
111
|
-
* No independent action (as in someone calling `copy_file` directly) will execute without its requirements being met.
|
112
|
-
* A shortcut to creating an Error value based on some condition, as well as capturing errors generated from testing the
|
113
|
-
condition can be seen in `ensure_file`. Any exceptions raised by File.exist?, as well as our condition not being met
|
114
|
-
are captured as Error
|
115
|
-
|
116
|
-
```ruby
|
117
|
-
require 'net/ftp'
|
118
|
-
|
119
|
-
class SensitiveFileFTP
|
120
|
-
def initialize(server_uri)
|
121
|
-
@server_uri = server_uri
|
122
|
-
end
|
123
|
-
|
124
|
-
def transfer_and_delete(file_name)
|
125
|
-
login & # "&" chains on Ok
|
126
|
-
copy_file(file_name) &
|
127
|
-
ensure_file(file_name) &
|
128
|
-
delete_remote_file(file_name)
|
129
|
-
end
|
130
|
-
|
131
|
-
def ftp
|
132
|
-
@ftp ||= Capture { Net::FTP.new(@server_uri) }
|
133
|
-
end
|
134
|
-
|
135
|
-
def login
|
136
|
-
ftp.map(&:login)
|
137
|
-
end
|
138
|
-
|
139
|
-
def copy_file(source)
|
140
|
-
ftp.map { |f| f.getbinaryfile(source) }
|
141
|
-
end
|
142
|
-
|
143
|
-
def ensure_file(name)
|
144
|
-
Capture { raise "Can not verify local copy of file" unless File.exist?(name) }
|
145
|
-
end
|
146
|
-
|
147
|
-
def delete_remote_file(name)
|
148
|
-
ftp.map {|f| f.delete(name) }
|
149
|
-
end
|
150
|
-
|
151
|
-
def delete_local_file(name)
|
152
|
-
Capture { File.delete(name) }
|
153
|
-
end
|
154
|
-
end
|
155
|
-
|
156
|
-
ftp = SensitiveFileFTP.new('speedtest.tele2.net')
|
157
|
-
ftp.transfer_and_delete("5MB.zip")
|
158
|
-
```
|
159
|
-
|
160
|
-
`transfer_and_delete` can return numerous errors but there is no way for this code to raise an exception. Exceptions
|
161
|
-
raised in `map` or `map!` are caught and expressed as `Error`.
|
162
|
-
|
163
|
-
#### Conditionally executing code
|
164
|
-
|
165
|
-
Using the example above, say we wanted to find and delete the file if we may have accidentally created it.
|
166
|
-
This wouldn't be a sensible way of solving this problem, but this illustrates a use case for result base conditionals.
|
167
|
-
|
168
|
-
```ruby
|
169
|
-
ftp = SensitiveFileFTP.new('speedtest.tele2.net')
|
170
|
-
ftp.transfer_and_delete("5MB.zip").or_else do
|
171
|
-
ftp.delete_local_file("5MB.zip")
|
172
|
-
end
|
173
|
-
|
174
|
-
# or lets say we wanted to log a success if it worked
|
175
|
-
|
176
|
-
ftp.transfer_and_delete("5MB.zip").and_then do
|
177
|
-
log "Did the thing!"
|
178
|
-
end
|
179
|
-
```
|
180
|
-
|
181
|
-
|
182
|
-
## What's next
|
183
|
-
|
184
|
-
* More documentation and refined use cases for existing methods, like `map_err`, `map!`, and `|`
|
185
|
-
|
186
|
-
* refine methods of conditionally executing blocks. Should the return values be wrapped in results, or should we trust
|
187
|
-
the users to do it?
|
188
|
-
|
189
|
-
* Right now, the only computational state kept is the last good result or the first error that occurred.
|
190
|
-
This may not be ideal or appropriate. We also don't join errors up the chain, so it is possible to end up with
|
191
|
-
something like this: `Error(Error(Error(Error(Error(...)))))`. We either need to find a way to join, and perhaps
|
192
|
-
store the errors as some aggregate object, or we need to provide utilities for probing for information on the different
|
193
|
-
nested errors. I think it may be appropriate to kep a collection of errors, and ensure that we don't nest.
|
194
|
-
|
195
|
-
|
196
90
|
## Development
|
197
91
|
|
198
92
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
data/lib/result-monad.rb
CHANGED
@@ -1,156 +1,30 @@
|
|
1
|
-
require
|
2
|
-
require '
|
1
|
+
require 'result-monad/version'
|
2
|
+
require 'result-monad/result_error'
|
3
|
+
require 'result-monad/result_methods'
|
4
|
+
require 'result-monad/error'
|
5
|
+
require 'result-monad/ok'
|
3
6
|
|
4
7
|
def Error(error)
|
5
|
-
|
8
|
+
Error.new(error)
|
6
9
|
end
|
7
10
|
|
8
11
|
def Ok(value)
|
9
|
-
|
12
|
+
Ok.new(value)
|
10
13
|
end
|
11
14
|
|
12
|
-
def
|
13
|
-
|
14
|
-
Ok(
|
15
|
-
|
16
|
-
Error(e)
|
15
|
+
def Test(o, e)
|
16
|
+
if yield
|
17
|
+
Ok(o)
|
18
|
+
else
|
19
|
+
Error(e)
|
17
20
|
end
|
18
21
|
end
|
19
22
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
@m = m
|
26
|
-
end
|
27
|
-
|
28
|
-
def ok?
|
29
|
-
@m.is_a? Ok
|
30
|
-
end
|
31
|
-
|
32
|
-
def error?
|
33
|
-
@m.is_a? Error
|
34
|
-
end
|
35
|
-
|
36
|
-
def map!(&block)
|
37
|
-
if ok?
|
38
|
-
@m.map!(&block)
|
39
|
-
self.lift!(val)
|
40
|
-
else
|
41
|
-
self
|
42
|
-
end
|
43
|
-
end
|
44
|
-
|
45
|
-
def map_err!(&block)
|
46
|
-
if error?
|
47
|
-
@m.map!(&block)
|
48
|
-
else
|
49
|
-
self
|
50
|
-
end
|
51
|
-
end
|
52
|
-
|
53
|
-
def map(&block)
|
54
|
-
if ok?
|
55
|
-
@m.map(&block)
|
56
|
-
else
|
57
|
-
self
|
58
|
-
end
|
59
|
-
end
|
60
|
-
|
61
|
-
def map_err(&block)
|
62
|
-
if error?
|
63
|
-
@m.map(&block)
|
64
|
-
else
|
65
|
-
self
|
66
|
-
end
|
67
|
-
end
|
68
|
-
|
69
|
-
def join!
|
70
|
-
if ok? && val.is_a?(Result)
|
71
|
-
lift!(val)
|
72
|
-
end
|
73
|
-
self
|
74
|
-
end
|
75
|
-
|
76
|
-
def lift!(other)
|
77
|
-
@m = (self & other.join!).instance_variable_get(:"@m")
|
78
|
-
self
|
79
|
-
end
|
80
|
-
|
81
|
-
def unwrap
|
82
|
-
raise ResultError.new(self), "Attempted to unwrap an error" if @m.is_a? Error
|
83
|
-
val
|
84
|
-
end
|
85
|
-
|
86
|
-
def and(other)
|
87
|
-
error? ? self : other
|
88
|
-
end
|
89
|
-
alias_method :&, :and
|
90
|
-
|
91
|
-
def or(other)
|
92
|
-
error? ? other : self
|
93
|
-
end
|
94
|
-
alias_method :|, :or
|
95
|
-
|
96
|
-
def and_then(&block)
|
97
|
-
Capture {block.call} if ok?
|
98
|
-
end
|
99
|
-
|
100
|
-
def or_else(&block)
|
101
|
-
Capture {block.call} if error?
|
102
|
-
end
|
103
|
-
|
104
|
-
def unwrap_or(cons)
|
105
|
-
ok? ? unwrap : cons
|
106
|
-
end
|
107
|
-
|
108
|
-
def map_when(kind, &block)
|
109
|
-
@m.map(&block) if val.is_a?(kind)
|
110
|
-
self
|
111
|
-
end
|
112
|
-
|
113
|
-
def do_when(kind, &block)
|
114
|
-
Capture { block.call } if val.is_a?(kind)
|
115
|
-
self
|
116
|
-
end
|
117
|
-
|
118
|
-
def raise_or_return!
|
119
|
-
if ok?
|
120
|
-
return val
|
121
|
-
else
|
122
|
-
raise val
|
123
|
-
end
|
124
|
-
end
|
125
|
-
|
126
|
-
# Constructors
|
127
|
-
def self.error(error)
|
128
|
-
new(Error.new(error))
|
129
|
-
end
|
130
|
-
|
131
|
-
def self.ok(result)
|
132
|
-
new(Ok.new(result))
|
133
|
-
end
|
134
|
-
|
135
|
-
def to_s
|
136
|
-
@m.to_s
|
137
|
-
end
|
138
|
-
|
139
|
-
def inspect
|
140
|
-
"Result<#{self}>"
|
141
|
-
end
|
142
|
-
|
143
|
-
private
|
144
|
-
|
145
|
-
def val
|
146
|
-
@m.instance_variable_get(:"@value")
|
23
|
+
def Capture
|
24
|
+
begin
|
25
|
+
Ok(yield)
|
26
|
+
rescue StandardError => e
|
27
|
+
Error(e)
|
147
28
|
end
|
148
29
|
end
|
149
30
|
|
150
|
-
class ResultError < StandardError
|
151
|
-
attr_reader :result
|
152
|
-
|
153
|
-
def initialize(result)
|
154
|
-
@result = result
|
155
|
-
end
|
156
|
-
end
|
data/lib/result-monad/error.rb
CHANGED
@@ -1,27 +1,13 @@
|
|
1
1
|
class Error
|
2
|
-
def initialize(value)
|
3
|
-
@value = value
|
4
|
-
end
|
5
2
|
|
6
|
-
|
7
|
-
@value = Capture {block.call(@value)}
|
8
|
-
self
|
9
|
-
end
|
3
|
+
include ResultMethods
|
10
4
|
|
11
|
-
def
|
12
|
-
|
5
|
+
def initialize(err)
|
6
|
+
@err = err
|
13
7
|
end
|
14
8
|
|
15
|
-
def
|
16
|
-
|
9
|
+
def handle_result(success_handler, failure_handler)
|
10
|
+
failure_handler.call(@err)
|
17
11
|
end
|
18
12
|
|
19
|
-
private
|
20
|
-
def capture_as_error
|
21
|
-
begin
|
22
|
-
Error(yield)
|
23
|
-
rescue StandardError => e
|
24
|
-
Error(e)
|
25
|
-
end
|
26
|
-
end
|
27
13
|
end
|
data/lib/result-monad/ok.rb
CHANGED
@@ -1,18 +1,13 @@
|
|
1
1
|
class Ok
|
2
|
-
def initialize(value)
|
3
|
-
@value = value
|
4
|
-
end
|
5
2
|
|
6
|
-
|
7
|
-
@value = Capture { block.call(@value) }
|
8
|
-
self
|
9
|
-
end
|
3
|
+
include ResultMethods
|
10
4
|
|
11
|
-
def
|
12
|
-
|
5
|
+
def initialize(result)
|
6
|
+
@result = result
|
13
7
|
end
|
14
8
|
|
15
|
-
def
|
16
|
-
|
9
|
+
def handle_result(success_handler, failure_handler)
|
10
|
+
success_handler.call(@result)
|
17
11
|
end
|
12
|
+
|
18
13
|
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module ResultMethods
|
2
|
+
#TODO:
|
3
|
+
# map! Not necessarily a good idea
|
4
|
+
# map_unsafe! See above
|
5
|
+
def map
|
6
|
+
handle_result(
|
7
|
+
Proc.new {|val| Capture { yield(val) } },
|
8
|
+
Proc.new {|err| self }
|
9
|
+
)
|
10
|
+
end
|
11
|
+
|
12
|
+
def map_err
|
13
|
+
handle_result(
|
14
|
+
Proc.new {|val| self },
|
15
|
+
Proc.new {|val| Capture { yield(val) } }
|
16
|
+
)
|
17
|
+
end
|
18
|
+
|
19
|
+
def join
|
20
|
+
handle_result(
|
21
|
+
Proc.new { |x| x.is_a?(Ok) || x.is_a?(Error) ? x.join : self },
|
22
|
+
Proc.new { |x| self }
|
23
|
+
)
|
24
|
+
end
|
25
|
+
|
26
|
+
def flat_map(&block)
|
27
|
+
(map &block).join
|
28
|
+
end
|
29
|
+
|
30
|
+
def map_unsafe
|
31
|
+
handle_result(
|
32
|
+
Proc.new {|val| yield(val) },
|
33
|
+
Proc.new {|err| self }
|
34
|
+
)
|
35
|
+
end
|
36
|
+
|
37
|
+
def and_then
|
38
|
+
handle_result(
|
39
|
+
Proc.new {|val| yield(val); self},
|
40
|
+
Proc.new {|err| self}
|
41
|
+
)
|
42
|
+
end
|
43
|
+
|
44
|
+
def or_else
|
45
|
+
handle_result(
|
46
|
+
Proc.new {|val| self},
|
47
|
+
Proc.new {|err| yield(val); self}
|
48
|
+
)
|
49
|
+
end
|
50
|
+
|
51
|
+
def flat_map_unsafe(&block)
|
52
|
+
(map_unsafe &block).join
|
53
|
+
end
|
54
|
+
|
55
|
+
def unwrap
|
56
|
+
handle_result(
|
57
|
+
Proc.new {|val| val},
|
58
|
+
Proc.new do |err|
|
59
|
+
if err.is_a? Exception
|
60
|
+
raise err
|
61
|
+
else
|
62
|
+
raise ResultError.new, "#{err}"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
)
|
66
|
+
end
|
67
|
+
|
68
|
+
def ok?
|
69
|
+
is_a? Ok
|
70
|
+
end
|
71
|
+
|
72
|
+
def error?
|
73
|
+
is_a? Error
|
74
|
+
end
|
75
|
+
|
76
|
+
def to_s
|
77
|
+
"#{ self.class.name }: #{@result}"
|
78
|
+
end
|
79
|
+
end
|
data/lib/result-monad/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: result-monad
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Zachary Daniel
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-02-
|
11
|
+
date: 2016-02-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -88,6 +88,8 @@ files:
|
|
88
88
|
- lib/result-monad.rb
|
89
89
|
- lib/result-monad/error.rb
|
90
90
|
- lib/result-monad/ok.rb
|
91
|
+
- lib/result-monad/result_error.rb
|
92
|
+
- lib/result-monad/result_methods.rb
|
91
93
|
- lib/result-monad/version.rb
|
92
94
|
- result-monad.gemspec
|
93
95
|
homepage: http://gitlab.com/zachdaniel/result-monad
|