kleisli 0.0.1
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 +7 -0
- data/.gitignore +14 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +275 -0
- data/Rakefile +10 -0
- data/kleisli.gemspec +23 -0
- data/lib/kleisli.rb +9 -0
- data/lib/kleisli/either.rb +60 -0
- data/lib/kleisli/functor.rb +7 -0
- data/lib/kleisli/future.rb +33 -0
- data/lib/kleisli/maybe.rb +55 -0
- data/lib/kleisli/monad.rb +9 -0
- data/lib/kleisli/monoid.rb +65 -0
- data/lib/kleisli/try.rb +56 -0
- data/lib/kleisli/version.rb +3 -0
- data/lib/kleisli/writer.rb +36 -0
- data/test/kleisli/either_test.rb +45 -0
- data/test/kleisli/future_test.rb +31 -0
- data/test/kleisli/maybe_test.rb +27 -0
- data/test/kleisli/monoid_test.rb +23 -0
- data/test/kleisli/try_test.rb +27 -0
- data/test/kleisli/writer_test.rb +19 -0
- data/test/test_helper.rb +2 -0
- metadata +104 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 3542c8f4abc331bb9a9d47170b22392146489666
|
4
|
+
data.tar.gz: 4381c9631f3205faffc3a3764340d3ce2c623041
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 52389420ffa39afd4fcb73731e95301d9258374e26f85afd1e467330cc61764d5b7c9989d0ef7309948cbc2134237ee5a8970325028f2fea67281b56ac8eb584
|
7
|
+
data.tar.gz: bf55220e96e5649b079588c661d2e7ff430197c8964bfa75971d78b7937fd29a29ae3b6a6e98c1b235d82b5490bd9eb97e4ff0baa4f4f17dffd91e360be66364
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Josep M. Bach
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,275 @@
|
|
1
|
+
# Kleisli
|
2
|
+
|
3
|
+
An idiomatic, clean implementation of a few common useful monads in Ruby, written by [Ryan Levick][rylev] and me.
|
4
|
+
|
5
|
+
It aims to be idiomatic Ruby to use in real app, not a proof of concept.
|
6
|
+
|
7
|
+
In your Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem 'kleisli'
|
11
|
+
```
|
12
|
+
|
13
|
+
We would like to thank Curry and Howard for their correspondence.
|
14
|
+
|
15
|
+
## Notation
|
16
|
+
|
17
|
+
For all its monads, Kleisli implements `return` (we call it `lift` instead, as `return` is a reserved keyword in Ruby) with convenience global methods (see which for each monad below).
|
18
|
+
|
19
|
+
Kleisli uses a clever Ruby syntax trick to implement the `bind` operator, which looks like this: `>->`. We will probably burn in hell for this.
|
20
|
+
|
21
|
+
## Maybe monad
|
22
|
+
|
23
|
+
The Maybe monad is useful to express a pipeline of computations that might
|
24
|
+
return nil at any point. `user.address.street` anyone?
|
25
|
+
|
26
|
+
### `>->` (bind)
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
require "kleisli"
|
30
|
+
|
31
|
+
maybe_user = Maybe(user) >-> user {
|
32
|
+
Maybe(user.address) } >-> address {
|
33
|
+
Maybe(address.street) }
|
34
|
+
|
35
|
+
# If user exists
|
36
|
+
# => Some("Monad Street")
|
37
|
+
# If user is nil
|
38
|
+
# => None()
|
39
|
+
|
40
|
+
# You can also use Some and None as type constructors yourself.
|
41
|
+
x = Some(10)
|
42
|
+
y = None()
|
43
|
+
```
|
44
|
+
|
45
|
+
### `fmap`
|
46
|
+
|
47
|
+
```ruby
|
48
|
+
require "kleisli"
|
49
|
+
|
50
|
+
# If we know that a user always has an address with a street
|
51
|
+
Maybe(user).fmap(&:address).fmap(&:street)
|
52
|
+
|
53
|
+
# If the user exists
|
54
|
+
# => Some("Monad Street")
|
55
|
+
# If the user is nil
|
56
|
+
# => None()
|
57
|
+
```
|
58
|
+
|
59
|
+
## Try
|
60
|
+
|
61
|
+
The Try monad is useful to express a pipeline of computations that might throw
|
62
|
+
an exception at any point.
|
63
|
+
|
64
|
+
### `>->` (bind)
|
65
|
+
|
66
|
+
```ruby
|
67
|
+
require "kleisli"
|
68
|
+
|
69
|
+
json_string = get_json_from_somewhere
|
70
|
+
|
71
|
+
result = Try { JSON.parse(json_string) } >-> json {
|
72
|
+
Try { json["dividend"].to_i / json["divisor"].to_i }
|
73
|
+
}
|
74
|
+
|
75
|
+
# If no exception was thrown:
|
76
|
+
|
77
|
+
result # => #<Try::Success @value=123>
|
78
|
+
result.value # => 123
|
79
|
+
|
80
|
+
# If there was a ZeroDivisionError exception for example:
|
81
|
+
|
82
|
+
result # => #<Try::Failure @exception=#<ZeroDivisionError ...>>
|
83
|
+
result.exception # => #<ZeroDivisionError ...>
|
84
|
+
```
|
85
|
+
|
86
|
+
### `fmap`
|
87
|
+
|
88
|
+
```ruby
|
89
|
+
require "kleisli"
|
90
|
+
|
91
|
+
Try { JSON.parse(json_string) }.fmap(&:symbolize_keys).value
|
92
|
+
|
93
|
+
# If everything went well:
|
94
|
+
# => { :my => "json", :with => "symbolized keys" }
|
95
|
+
# If an exception was thrown:
|
96
|
+
# => nil
|
97
|
+
```
|
98
|
+
|
99
|
+
### `to_maybe`
|
100
|
+
|
101
|
+
Sometimes it's useful to interleave both `Try` and `Maybe`. To convert a `Try`
|
102
|
+
into a `Maybe` you can use `to_maybe`:
|
103
|
+
|
104
|
+
```ruby
|
105
|
+
require "kleisli"
|
106
|
+
|
107
|
+
Try { JSON.parse(json_string) }.fmap(&:symbolize_keys).to_maybe
|
108
|
+
|
109
|
+
# If everything went well:
|
110
|
+
# => Some({ :my => "json", :with => "symbolized keys" })
|
111
|
+
# If an exception was thrown:
|
112
|
+
# => None()
|
113
|
+
```
|
114
|
+
|
115
|
+
## Either
|
116
|
+
|
117
|
+
The Either monad is useful to express a pipeline of computations that might return an error object with some information.
|
118
|
+
|
119
|
+
It has two type constructors: `Right` and `Left`. As a useful mnemonic, `Right` is for when everything went "right" and `Left` is used for errors.
|
120
|
+
|
121
|
+
Think of it as exceptions without messing with the call stack.
|
122
|
+
|
123
|
+
### `>->` (bind)
|
124
|
+
|
125
|
+
```ruby
|
126
|
+
require "kleisli"
|
127
|
+
|
128
|
+
result = Right(3) >-> value {
|
129
|
+
if value > 1
|
130
|
+
Right(value + 3)
|
131
|
+
else
|
132
|
+
Left("value was less or equal than 1")
|
133
|
+
end
|
134
|
+
} >-> value {
|
135
|
+
if value % 2 == 0
|
136
|
+
Right(value * 2)
|
137
|
+
else
|
138
|
+
Left("value was not even")
|
139
|
+
end
|
140
|
+
}
|
141
|
+
|
142
|
+
# If everything went well
|
143
|
+
result # => Right(12)
|
144
|
+
result.value # => 12
|
145
|
+
|
146
|
+
# If it failed in the first block
|
147
|
+
result # => Left("value was less or equal than 1")
|
148
|
+
result.value # => "value was less or equal than 1"
|
149
|
+
|
150
|
+
# If it failed in the second block
|
151
|
+
result # => Left("value was not even")
|
152
|
+
result.value # => "value was not even"
|
153
|
+
```
|
154
|
+
|
155
|
+
### `fmap`
|
156
|
+
|
157
|
+
```ruby
|
158
|
+
require "kleisli"
|
159
|
+
|
160
|
+
result = if foo > bar
|
161
|
+
Right(10)
|
162
|
+
else
|
163
|
+
Left("wrong")
|
164
|
+
end.fmap { |x| x * 2 }
|
165
|
+
|
166
|
+
# If everything went well
|
167
|
+
result # => Right(20)
|
168
|
+
# If it didn't
|
169
|
+
result # => Left("wrong")
|
170
|
+
```
|
171
|
+
|
172
|
+
### `to_maybe`
|
173
|
+
|
174
|
+
Sometimes it's useful to turn an `Either` into a `Maybe`. You can use
|
175
|
+
`to_maybe` for that:
|
176
|
+
|
177
|
+
```ruby
|
178
|
+
require "kleisli"
|
179
|
+
|
180
|
+
result = if foo > bar
|
181
|
+
Right(10)
|
182
|
+
else
|
183
|
+
Left("wrong")
|
184
|
+
end.to_maybe
|
185
|
+
|
186
|
+
# If everything went well:
|
187
|
+
result # => Some(10)
|
188
|
+
# If it didn't
|
189
|
+
result # => None()
|
190
|
+
```
|
191
|
+
|
192
|
+
## Writer
|
193
|
+
|
194
|
+
The Writer monad is arguably the least useful monad in Ruby (as side effects
|
195
|
+
are uncontrolled), but let's take a look at it anyway.
|
196
|
+
|
197
|
+
It is used to model computations that *append* to some kind of state (which
|
198
|
+
needs to be a Monoid, expressed in the `Kleisli::Monoid` mixin) at each step.
|
199
|
+
|
200
|
+
(We've already implemented the Monoid interface for `String`, `Array`, `Hash`,
|
201
|
+
`Fixnum` and `Float` for you.)
|
202
|
+
|
203
|
+
An example would be a pipeline of computations that append to a log, for
|
204
|
+
example a list of strings.
|
205
|
+
|
206
|
+
### `>->` (bind)
|
207
|
+
|
208
|
+
```ruby
|
209
|
+
require "kleisli"
|
210
|
+
|
211
|
+
writer = Writer([], 100) >-> value {
|
212
|
+
Writer(["added 100"], value + 100)
|
213
|
+
} >-> value {
|
214
|
+
Writer(["added 140 more"], value + 140)
|
215
|
+
} # => Writer(["added 100", "added 140 more"], 340)
|
216
|
+
|
217
|
+
log, value = writer.unwrap
|
218
|
+
log # => ["added 100", "added 140 more"]
|
219
|
+
value # => 340
|
220
|
+
```
|
221
|
+
|
222
|
+
### `fmap`
|
223
|
+
|
224
|
+
```ruby
|
225
|
+
require "kleisli"
|
226
|
+
|
227
|
+
writer = Writer([], 100).fmap { |value|
|
228
|
+
value + 100
|
229
|
+
} # => Writer([], 200)
|
230
|
+
```
|
231
|
+
|
232
|
+
## Future
|
233
|
+
|
234
|
+
The Future monad models a pipeline of computations that will happen in the future, as soon as the value needed for each step is available. It is useful to model, for example, a sequential chain of HTTP calls.
|
235
|
+
|
236
|
+
### `>->` (bind)
|
237
|
+
|
238
|
+
```ruby
|
239
|
+
require "kleisli"
|
240
|
+
|
241
|
+
f = Future("myendpoint.com") >-> url {
|
242
|
+
Future { HTTP.get(url.call) }
|
243
|
+
} >-> response {
|
244
|
+
Future {
|
245
|
+
other_url = JSON.parse(response.call.body)[:other_url]
|
246
|
+
HTTP.get(other_url)
|
247
|
+
}
|
248
|
+
} >-> other_response {
|
249
|
+
Future { JSON.parse(other_response.call.body) }
|
250
|
+
}
|
251
|
+
|
252
|
+
# Do some other stuff...
|
253
|
+
|
254
|
+
f.await # => block until the whole pipeline is realized
|
255
|
+
# => { "my" => "response body" }
|
256
|
+
```
|
257
|
+
|
258
|
+
### `fmap`
|
259
|
+
|
260
|
+
```ruby
|
261
|
+
require "kleisli"
|
262
|
+
|
263
|
+
Future { expensive_operation }.fmap { |x| x * 2 }.await
|
264
|
+
# => result of expensive_operation * 2
|
265
|
+
```
|
266
|
+
|
267
|
+
## Who's this
|
268
|
+
|
269
|
+
This was made by [Josep M. Bach (Txus)](http://blog.txus.io) and [Ryan
|
270
|
+
Levick][rylev] under the MIT license. We are [@txustice][twitter] and
|
271
|
+
[@itchyankles][itchyankles] on twitter (where you should probably follow us!).
|
272
|
+
|
273
|
+
[twitter]: https://twitter.com/txustice
|
274
|
+
[itchyankles]: https://twitter.com/itchyankles
|
275
|
+
[rylev]: https://github.com/rylev
|
data/Rakefile
ADDED
data/kleisli.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'kleisli/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "kleisli"
|
8
|
+
spec.version = Kleisli::VERSION
|
9
|
+
spec.authors = ["Josep M. Bach", "Ryan Levick"]
|
10
|
+
spec.email = ["josep.m.bach@gmail.com", "ryan.levick@gmail.com"]
|
11
|
+
spec.summary = %q{Usable, idiomatic common monads in Ruby}
|
12
|
+
spec.description = %q{Usable, idiomatic common monads in Ruby}
|
13
|
+
spec.homepage = "https://github.com/txus/kleisli"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.7"
|
22
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
23
|
+
end
|
data/lib/kleisli.rb
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'kleisli/monad'
|
2
|
+
require 'kleisli/maybe'
|
3
|
+
|
4
|
+
module Kleisli
|
5
|
+
class Either < Monad
|
6
|
+
attr_reader :right, :left
|
7
|
+
|
8
|
+
def ==(other)
|
9
|
+
right == other.right && left == other.left
|
10
|
+
end
|
11
|
+
|
12
|
+
class Right < Either
|
13
|
+
alias value right
|
14
|
+
|
15
|
+
def initialize(right)
|
16
|
+
@right = right
|
17
|
+
end
|
18
|
+
|
19
|
+
def >(f)
|
20
|
+
f.call(@right)
|
21
|
+
end
|
22
|
+
|
23
|
+
def fmap(&f)
|
24
|
+
Right.new(f.call(@right))
|
25
|
+
end
|
26
|
+
|
27
|
+
def to_maybe
|
28
|
+
Maybe::Some.new(@right)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
class Left < Either
|
33
|
+
alias value left
|
34
|
+
|
35
|
+
def initialize(left)
|
36
|
+
@left = left
|
37
|
+
end
|
38
|
+
|
39
|
+
def >(f)
|
40
|
+
self
|
41
|
+
end
|
42
|
+
|
43
|
+
def fmap(&f)
|
44
|
+
self
|
45
|
+
end
|
46
|
+
|
47
|
+
def to_maybe
|
48
|
+
Maybe::None.new
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def Right(v)
|
55
|
+
Kleisli::Either::Right.new(v)
|
56
|
+
end
|
57
|
+
|
58
|
+
def Left(v)
|
59
|
+
Kleisli::Either::Left.new(v)
|
60
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'kleisli/monad'
|
2
|
+
|
3
|
+
module Kleisli
|
4
|
+
class Future < Monad
|
5
|
+
def self.lift(v=nil, &block)
|
6
|
+
if block
|
7
|
+
new(Thread.new(&block))
|
8
|
+
else
|
9
|
+
new(Thread.new { v })
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(t)
|
14
|
+
@t = t
|
15
|
+
end
|
16
|
+
|
17
|
+
def >(f)
|
18
|
+
f.call(-> { await })
|
19
|
+
end
|
20
|
+
|
21
|
+
def fmap(&f)
|
22
|
+
Future.lift(f.call(-> { await }))
|
23
|
+
end
|
24
|
+
|
25
|
+
def await
|
26
|
+
@t.join.value
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def Future(v=nil, &block)
|
32
|
+
Kleisli::Future.lift(v, &block)
|
33
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'kleisli/monad'
|
2
|
+
|
3
|
+
module Kleisli
|
4
|
+
class Maybe < Monad
|
5
|
+
attr_reader :value
|
6
|
+
|
7
|
+
def self.lift(value)
|
8
|
+
if value.nil?
|
9
|
+
None.new
|
10
|
+
else
|
11
|
+
Some.new(value)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def ==(other)
|
16
|
+
value == other.value
|
17
|
+
end
|
18
|
+
|
19
|
+
class None < Maybe
|
20
|
+
def fmap(&f)
|
21
|
+
self
|
22
|
+
end
|
23
|
+
|
24
|
+
def >(block)
|
25
|
+
self
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class Some < Maybe
|
30
|
+
def initialize(value)
|
31
|
+
@value = value
|
32
|
+
end
|
33
|
+
|
34
|
+
def fmap(&f)
|
35
|
+
Maybe.lift(f.call(@value))
|
36
|
+
end
|
37
|
+
|
38
|
+
def >(block)
|
39
|
+
block.call(@value)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def Maybe(v)
|
46
|
+
Kleisli::Maybe.lift(v)
|
47
|
+
end
|
48
|
+
|
49
|
+
def None()
|
50
|
+
Maybe(nil)
|
51
|
+
end
|
52
|
+
|
53
|
+
def Some(v)
|
54
|
+
Maybe(v)
|
55
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module Kleisli
|
2
|
+
module Monoid
|
3
|
+
def fold(others)
|
4
|
+
others.reduce(self) { |acc, x| acc.mappend x }
|
5
|
+
end
|
6
|
+
|
7
|
+
def mempty
|
8
|
+
raise NotImplementedError, "this monoid doesn't implement mpemty"
|
9
|
+
end
|
10
|
+
|
11
|
+
def mappend(other)
|
12
|
+
raise NotImplementedError, "this monoid doesn't implement mappend"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
String.class_eval do
|
18
|
+
include Kleisli::Monoid
|
19
|
+
|
20
|
+
def mempty
|
21
|
+
""
|
22
|
+
end
|
23
|
+
|
24
|
+
alias mappend +
|
25
|
+
end
|
26
|
+
|
27
|
+
Array.class_eval do
|
28
|
+
include Kleisli::Monoid
|
29
|
+
|
30
|
+
def mempty
|
31
|
+
[]
|
32
|
+
end
|
33
|
+
|
34
|
+
alias mappend +
|
35
|
+
end
|
36
|
+
|
37
|
+
Hash.class_eval do
|
38
|
+
include Kleisli::Monoid
|
39
|
+
|
40
|
+
def mempty
|
41
|
+
{}
|
42
|
+
end
|
43
|
+
|
44
|
+
alias mappend merge
|
45
|
+
end
|
46
|
+
|
47
|
+
Fixnum.class_eval do
|
48
|
+
include Kleisli::Monoid
|
49
|
+
|
50
|
+
def mempty
|
51
|
+
0
|
52
|
+
end
|
53
|
+
|
54
|
+
alias mappend +
|
55
|
+
end
|
56
|
+
|
57
|
+
Float.class_eval do
|
58
|
+
include Kleisli::Monoid
|
59
|
+
|
60
|
+
def mempty
|
61
|
+
0
|
62
|
+
end
|
63
|
+
|
64
|
+
alias mappend +
|
65
|
+
end
|
data/lib/kleisli/try.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'kleisli/monad'
|
2
|
+
require 'kleisli/maybe'
|
3
|
+
|
4
|
+
module Kleisli
|
5
|
+
class Try < Monad
|
6
|
+
attr_reader :exception, :value
|
7
|
+
|
8
|
+
def self.lift(f)
|
9
|
+
Success.new(f.call)
|
10
|
+
rescue => e
|
11
|
+
Failure.new(e)
|
12
|
+
end
|
13
|
+
|
14
|
+
class Success < Try
|
15
|
+
def initialize(value)
|
16
|
+
@value = value
|
17
|
+
end
|
18
|
+
|
19
|
+
def >(f)
|
20
|
+
f.call(@value)
|
21
|
+
rescue => e
|
22
|
+
Failure.new(e)
|
23
|
+
end
|
24
|
+
|
25
|
+
def fmap(&f)
|
26
|
+
Try { f.call(@value) }
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_maybe
|
30
|
+
Maybe::Some.new(@value)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class Failure < Try
|
35
|
+
def initialize(exception)
|
36
|
+
@exception = exception
|
37
|
+
end
|
38
|
+
|
39
|
+
def >(f)
|
40
|
+
self
|
41
|
+
end
|
42
|
+
|
43
|
+
def fmap(&f)
|
44
|
+
self
|
45
|
+
end
|
46
|
+
|
47
|
+
def to_maybe
|
48
|
+
Maybe::None.new
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def Try(&f)
|
55
|
+
Kleisli::Try.lift(f)
|
56
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'kleisli/monad'
|
2
|
+
require 'kleisli/monoid'
|
3
|
+
|
4
|
+
module Kleisli
|
5
|
+
class Writer < Monad
|
6
|
+
def self.lift(log, value)
|
7
|
+
new(log, value)
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(log, value)
|
11
|
+
@log, @value = log, value
|
12
|
+
end
|
13
|
+
|
14
|
+
def ==(other)
|
15
|
+
unwrap == other.unwrap
|
16
|
+
end
|
17
|
+
|
18
|
+
def >(f)
|
19
|
+
other_log, other_value = f.call(@value).unwrap
|
20
|
+
Writer.new(@log + other_log, other_value)
|
21
|
+
end
|
22
|
+
|
23
|
+
def fmap(&f)
|
24
|
+
new_value = f.call(@value)
|
25
|
+
Writer.new(@log, new_value)
|
26
|
+
end
|
27
|
+
|
28
|
+
def unwrap
|
29
|
+
[@log, @value]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def Writer(log, value)
|
35
|
+
Kleisli::Writer.lift(log, value)
|
36
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class EitherTest < MiniTest::Unit::TestCase
|
4
|
+
def test_lift_right
|
5
|
+
assert_equal 3, Right(3).value
|
6
|
+
end
|
7
|
+
|
8
|
+
def test_lift_left
|
9
|
+
assert_equal "error", Left("error").value
|
10
|
+
end
|
11
|
+
|
12
|
+
def test_bind_right
|
13
|
+
v = Right(1) >-> x {
|
14
|
+
if x == 1
|
15
|
+
Right(x + 90)
|
16
|
+
else
|
17
|
+
Left("FAIL")
|
18
|
+
end
|
19
|
+
}
|
20
|
+
assert_equal Right(91), v
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_bind_left
|
24
|
+
v = Left("error") >-> x {
|
25
|
+
Right(x * 20)
|
26
|
+
}
|
27
|
+
assert_equal Left("error"), v
|
28
|
+
end
|
29
|
+
|
30
|
+
def test_fmap_right
|
31
|
+
assert_equal Right(2), Right(1).fmap { |x| x * 2 }
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_fmap_left
|
35
|
+
assert_equal Left("error"), Left("error").fmap { |x| x * 2 }
|
36
|
+
end
|
37
|
+
|
38
|
+
def test_to_maybe_right
|
39
|
+
assert_equal Some(2), Right(1).fmap { |x| x * 2 }.to_maybe
|
40
|
+
end
|
41
|
+
|
42
|
+
def test_to_maybe_left
|
43
|
+
assert_equal None(), Left("error").fmap { |x| x * 2 }.to_maybe
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class FutureTest < MiniTest::Unit::TestCase
|
4
|
+
def test_immediate_value
|
5
|
+
assert_equal 30, Future(30).await
|
6
|
+
end
|
7
|
+
|
8
|
+
def test_simple_future_executes_in_parallel
|
9
|
+
str = ""
|
10
|
+
Future { sleep 0.1; str << "bar" }.tap {
|
11
|
+
str << "foo"
|
12
|
+
}.await
|
13
|
+
assert_equal "foobar", str
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_bind
|
17
|
+
f = Future(30) >-> n {
|
18
|
+
Future { n.call * 2 }
|
19
|
+
} >-> n {
|
20
|
+
Future { n.call * 2 } >-> m {
|
21
|
+
Future(m.call + 2)
|
22
|
+
}
|
23
|
+
}
|
24
|
+
assert_equal 122, f.await
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_fmap
|
28
|
+
f = Future(30).fmap { |x| x.call * 2 }
|
29
|
+
assert_equal 60, f.await
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class MaybeTest < MiniTest::Unit::TestCase
|
4
|
+
def test_unwrapping_some
|
5
|
+
assert_equal 3, Some(3).value
|
6
|
+
end
|
7
|
+
|
8
|
+
def test_unwrapping_none
|
9
|
+
assert_equal nil, None().value
|
10
|
+
end
|
11
|
+
|
12
|
+
def test_bind_none
|
13
|
+
assert_equal None(), None() >-> x { Maybe(x * 2) }
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_bind_some
|
17
|
+
assert_equal Some(6), Some(3) >-> x { Maybe(x * 2) }
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_fmap_none
|
21
|
+
assert_equal None(), None().fmap { |x| x * 2 }
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_fmap_some
|
25
|
+
assert_equal Some(6), Some(3).fmap { |x| x * 2 }
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class MonoidTest < MiniTest::Unit::TestCase
|
4
|
+
def test_string_fold
|
5
|
+
assert_equal "hellogoodbye", "hello".fold(%w(good bye))
|
6
|
+
end
|
7
|
+
|
8
|
+
def test_array_fold
|
9
|
+
assert_equal [1, 2, 3], [1].fold([[2], [3]])
|
10
|
+
end
|
11
|
+
|
12
|
+
def test_hash_fold
|
13
|
+
assert_equal({a: 1, b: 2, c: 3}, {a: 1}.fold([{b: 2}, {c: 3}]))
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_fixnum_fold
|
17
|
+
assert_equal 6, 1.fold([2,3])
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_float_fold
|
21
|
+
assert_equal 6.0, 1.0.fold([2.0,3.0])
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class TryTest < MiniTest::Unit::TestCase
|
4
|
+
def test_success
|
5
|
+
assert_equal 2, Try { 10 / 5 }.value
|
6
|
+
end
|
7
|
+
|
8
|
+
def test_failure
|
9
|
+
assert_kind_of ZeroDivisionError, Try { 10 / 0 }.exception
|
10
|
+
end
|
11
|
+
|
12
|
+
def test_to_maybe_success
|
13
|
+
assert_equal Some(2), Try { 10 / 5 }.to_maybe
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_to_maybe_failure
|
17
|
+
assert_equal None(), Try { 10 / 0 }.to_maybe
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_fmap_success
|
21
|
+
assert_equal 4, Try { 10 / 5 }.fmap { |x| x * 2 }.value
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_fmap_failure
|
25
|
+
assert_kind_of ZeroDivisionError, Try { 10 / 0 }.fmap { |x| x * 2 }.exception
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class WriterTest < MiniTest::Unit::TestCase
|
4
|
+
def test_unwrap
|
5
|
+
log, value = Writer("log", 100).unwrap
|
6
|
+
assert_equal "log", log
|
7
|
+
assert_equal 100, value
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_bind
|
11
|
+
writer = Writer("foo", 100) >-> value { Writer("bar", value + 100) }
|
12
|
+
assert_equal Writer("foobar", 200), writer
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_fmap
|
16
|
+
writer = Writer("foo", 100).fmap { |value| value + 100 }
|
17
|
+
assert_equal Writer("foo", 200), writer
|
18
|
+
end
|
19
|
+
end
|
data/test/test_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: kleisli
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Josep M. Bach
|
8
|
+
- Ryan Levick
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2014-10-31 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: bundler
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - "~>"
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: '1.7'
|
21
|
+
type: :development
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - "~>"
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: '1.7'
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: rake
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - "~>"
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: '10.0'
|
35
|
+
type: :development
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - "~>"
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '10.0'
|
42
|
+
description: Usable, idiomatic common monads in Ruby
|
43
|
+
email:
|
44
|
+
- josep.m.bach@gmail.com
|
45
|
+
- ryan.levick@gmail.com
|
46
|
+
executables: []
|
47
|
+
extensions: []
|
48
|
+
extra_rdoc_files: []
|
49
|
+
files:
|
50
|
+
- ".gitignore"
|
51
|
+
- Gemfile
|
52
|
+
- LICENSE.txt
|
53
|
+
- README.md
|
54
|
+
- Rakefile
|
55
|
+
- kleisli.gemspec
|
56
|
+
- lib/kleisli.rb
|
57
|
+
- lib/kleisli/either.rb
|
58
|
+
- lib/kleisli/functor.rb
|
59
|
+
- lib/kleisli/future.rb
|
60
|
+
- lib/kleisli/maybe.rb
|
61
|
+
- lib/kleisli/monad.rb
|
62
|
+
- lib/kleisli/monoid.rb
|
63
|
+
- lib/kleisli/try.rb
|
64
|
+
- lib/kleisli/version.rb
|
65
|
+
- lib/kleisli/writer.rb
|
66
|
+
- test/kleisli/either_test.rb
|
67
|
+
- test/kleisli/future_test.rb
|
68
|
+
- test/kleisli/maybe_test.rb
|
69
|
+
- test/kleisli/monoid_test.rb
|
70
|
+
- test/kleisli/try_test.rb
|
71
|
+
- test/kleisli/writer_test.rb
|
72
|
+
- test/test_helper.rb
|
73
|
+
homepage: https://github.com/txus/kleisli
|
74
|
+
licenses:
|
75
|
+
- MIT
|
76
|
+
metadata: {}
|
77
|
+
post_install_message:
|
78
|
+
rdoc_options: []
|
79
|
+
require_paths:
|
80
|
+
- lib
|
81
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
82
|
+
requirements:
|
83
|
+
- - ">="
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
87
|
+
requirements:
|
88
|
+
- - ">="
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: '0'
|
91
|
+
requirements: []
|
92
|
+
rubyforge_project:
|
93
|
+
rubygems_version: 2.2.2
|
94
|
+
signing_key:
|
95
|
+
specification_version: 4
|
96
|
+
summary: Usable, idiomatic common monads in Ruby
|
97
|
+
test_files:
|
98
|
+
- test/kleisli/either_test.rb
|
99
|
+
- test/kleisli/future_test.rb
|
100
|
+
- test/kleisli/maybe_test.rb
|
101
|
+
- test/kleisli/monoid_test.rb
|
102
|
+
- test/kleisli/try_test.rb
|
103
|
+
- test/kleisli/writer_test.rb
|
104
|
+
- test/test_helper.rb
|