monads 0.0.1 → 0.1.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/.travis.yml +11 -0
- data/README.md +98 -0
- data/lib/monads/eventually.rb +8 -2
- data/lib/monads/many.rb +11 -1
- data/lib/monads/monad.rb +1 -1
- data/lib/monads/optional.rb +11 -1
- data/lib/monads/version.rb +1 -1
- data/spec/monads/eventually_spec.rb +33 -0
- data/spec/monads/many_spec.rb +35 -0
- data/spec/monads/optional_spec.rb +35 -0
- metadata +5 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 87404c6f7a69fed0181330dcaefafe2760574f36
|
4
|
+
data.tar.gz: 210ed8c40e603d2e0e9d13151170c37ab83443c2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 42898713212d672e5a774693ea259b1a62c2a274442cf141ab8bc1c97a85a7d8892589e62a480d5d437b7e6f78bfe22f60bceaa5bc50ff1b67ecc2bc9ce4989f
|
7
|
+
data.tar.gz: 43306a7166f3a1a653d2dfb8461bde9d8b4583a0c6ce08bba3b506147e5ebeecac4e5a41868a6d201e101fea4d21cab2ff9b679983cd6a50e23b80688bfef07c
|
data/.travis.yml
ADDED
data/README.md
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
# Monads
|
2
|
+
|
3
|
+
[](https://travis-ci.org/tomstuart/monads)
|
4
|
+
|
5
|
+
This library provides simple Ruby implementations of some common [monads](http://en.wikipedia.org/wiki/Monad_(functional_programming)).
|
6
|
+
|
7
|
+
The most important method of each implementation is `#and_then` (a.k.a. `bind` or `>>=`), which is used to connect together a sequence of operations involving the value(s) inside the monad. Each monad also has a `.from_value` class method (a.k.a. `return` or `unit`) for constructing an instance of that monad from an arbitrary value.
|
8
|
+
|
9
|
+
## `Optional`
|
10
|
+
|
11
|
+
(a.k.a. the [maybe monad](http://en.wikipedia.org/wiki/Monad_%28functional_programming%29#The_Maybe_monad))
|
12
|
+
|
13
|
+
An `Optional` object contains a value that might be `nil`.
|
14
|
+
|
15
|
+
```irb
|
16
|
+
>> require 'monads/optional'
|
17
|
+
=> true
|
18
|
+
|
19
|
+
>> include Monads
|
20
|
+
=> Object
|
21
|
+
|
22
|
+
>> optional_string = Optional.new('hello world')
|
23
|
+
=> #<struct Monads::Optional value="hello world">
|
24
|
+
|
25
|
+
>> optional_result = optional_string.and_then { |string| Optional.new(string.upcase) }
|
26
|
+
=> #<struct Monads::Optional value="HELLO WORLD">
|
27
|
+
|
28
|
+
>> optional_result.value
|
29
|
+
=> "HELLO WORLD"
|
30
|
+
|
31
|
+
>> optional_string = Optional.new(nil)
|
32
|
+
=> #<struct Monads::Optional value=nil>
|
33
|
+
|
34
|
+
>> optional_result = optional_string.and_then { |string| Optional.new(string.upcase) }
|
35
|
+
=> #<struct Monads::Optional value=nil>
|
36
|
+
|
37
|
+
>> optional_result.value
|
38
|
+
=> nil
|
39
|
+
```
|
40
|
+
|
41
|
+
## `Many`
|
42
|
+
|
43
|
+
(a.k.a. the [list monad](http://en.wikipedia.org/wiki/Monad_%28functional_programming%29#Collections))
|
44
|
+
|
45
|
+
A `Many` object contains multiple values.
|
46
|
+
|
47
|
+
```irb
|
48
|
+
>> require 'monads/many'
|
49
|
+
=> true
|
50
|
+
|
51
|
+
>> include Monads
|
52
|
+
=> Object
|
53
|
+
|
54
|
+
>> many_strings = Many.new(['hello world', 'goodbye world'])
|
55
|
+
=> #<struct Monads::Many values=["hello world", "goodbye world"]>
|
56
|
+
|
57
|
+
>> many_results = many_strings.and_then { |string| Many.new(string.split(/ /)) }
|
58
|
+
=> #<struct Monads::Many values=["hello", "world", "goodbye", "world"]>
|
59
|
+
|
60
|
+
>> many_results.values
|
61
|
+
=> ["hello", "world", "goodbye", "world"]
|
62
|
+
```
|
63
|
+
|
64
|
+
## `Eventually`
|
65
|
+
|
66
|
+
(a.k.a. the [continuation monad](http://en.wikipedia.org/wiki/Monad_%28functional_programming%29#Continuation_monad))
|
67
|
+
|
68
|
+
An `Eventually` object contains a value that will eventually be available, perhaps as the result of an asynchronous process (e.g. a network request).
|
69
|
+
|
70
|
+
```irb
|
71
|
+
>> require 'monads/eventually'
|
72
|
+
=> true
|
73
|
+
|
74
|
+
>> include Monads
|
75
|
+
=> Object
|
76
|
+
|
77
|
+
>> eventually_string = Eventually.new do |success|
|
78
|
+
Thread.new do
|
79
|
+
sleep 5
|
80
|
+
success.call('hello world')
|
81
|
+
end
|
82
|
+
end
|
83
|
+
=> #<struct Monads::Eventually block=#<Proc>>
|
84
|
+
|
85
|
+
>> eventually_result = eventually_string.and_then do |string|
|
86
|
+
Eventually.new do |success|
|
87
|
+
Thread.new do
|
88
|
+
sleep 5
|
89
|
+
success.call(string.upcase)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
=> #<struct Monads::Eventually block=#<Proc>>
|
94
|
+
|
95
|
+
>> eventually_result.run { |string| puts string }
|
96
|
+
=> #<Thread run>
|
97
|
+
HELLO WORLD
|
98
|
+
```
|
data/lib/monads/eventually.rb
CHANGED
@@ -1,11 +1,13 @@
|
|
1
1
|
require 'monads/monad'
|
2
2
|
|
3
3
|
module Monads
|
4
|
-
Eventually
|
4
|
+
class Eventually
|
5
5
|
include Monad
|
6
6
|
|
7
|
+
attr_reader :block
|
8
|
+
|
7
9
|
def initialize(&block)
|
8
|
-
|
10
|
+
@block = block
|
9
11
|
end
|
10
12
|
|
11
13
|
def run(&success)
|
@@ -22,6 +24,10 @@ module Monads
|
|
22
24
|
end
|
23
25
|
end
|
24
26
|
|
27
|
+
def respond_to_missing?(method_name, include_private = false)
|
28
|
+
super || run { |value| value.respond_to?(method_name, include_private) }
|
29
|
+
end
|
30
|
+
|
25
31
|
def self.from_value(value)
|
26
32
|
Eventually.new do |success|
|
27
33
|
success.call(value)
|
data/lib/monads/many.rb
CHANGED
@@ -1,15 +1,25 @@
|
|
1
1
|
require 'monads/monad'
|
2
2
|
|
3
3
|
module Monads
|
4
|
-
Many
|
4
|
+
class Many
|
5
5
|
include Monad
|
6
6
|
|
7
|
+
attr_reader :values
|
8
|
+
|
9
|
+
def initialize(values)
|
10
|
+
@values = values
|
11
|
+
end
|
12
|
+
|
7
13
|
def and_then(&block)
|
8
14
|
block = ensure_monadic_result(&block)
|
9
15
|
|
10
16
|
Many.new(values.map(&block).flat_map(&:values))
|
11
17
|
end
|
12
18
|
|
19
|
+
def respond_to_missing?(method_name, include_private = false)
|
20
|
+
super || values.all? { |value| value.respond_to?(method_name, include_private) }
|
21
|
+
end
|
22
|
+
|
13
23
|
def self.from_value(value)
|
14
24
|
Many.new([value])
|
15
25
|
end
|
data/lib/monads/monad.rb
CHANGED
@@ -17,7 +17,7 @@ module Monads
|
|
17
17
|
def ensure_monadic_result(&block)
|
18
18
|
acceptable_result_type = self.class
|
19
19
|
|
20
|
-
->
|
20
|
+
->(*a, &b) do
|
21
21
|
block.call(*a, &b).tap do |result|
|
22
22
|
unless result.is_a?(acceptable_result_type)
|
23
23
|
raise TypeError, "block must return #{acceptable_result_type.name}"
|
data/lib/monads/optional.rb
CHANGED
@@ -1,9 +1,15 @@
|
|
1
1
|
require 'monads/monad'
|
2
2
|
|
3
3
|
module Monads
|
4
|
-
Optional
|
4
|
+
class Optional
|
5
5
|
include Monad
|
6
6
|
|
7
|
+
attr_reader :value
|
8
|
+
|
9
|
+
def initialize(value)
|
10
|
+
@value = value
|
11
|
+
end
|
12
|
+
|
7
13
|
def and_then(&block)
|
8
14
|
block = ensure_monadic_result(&block)
|
9
15
|
|
@@ -14,6 +20,10 @@ module Monads
|
|
14
20
|
end
|
15
21
|
end
|
16
22
|
|
23
|
+
def respond_to_missing?(method_name, include_private = false)
|
24
|
+
super || value.nil? || value.respond_to?(method_name, include_private)
|
25
|
+
end
|
26
|
+
|
17
27
|
def self.from_value(value)
|
18
28
|
Optional.new(value)
|
19
29
|
end
|
data/lib/monads/version.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
1
3
|
require 'monads/eventually'
|
2
4
|
|
3
5
|
module Monads
|
@@ -107,6 +109,37 @@ module Monads
|
|
107
109
|
Eventually.new { |success| success.call(value) }.challenge.run { |result| @result = result }
|
108
110
|
expect(@result).to eq response
|
109
111
|
end
|
112
|
+
|
113
|
+
context 'when the value responds to the message' do
|
114
|
+
it 'reports that the Eventually responds to the message' do
|
115
|
+
expect(Eventually.new { |success| success.call(value) }).to respond_to(:challenge)
|
116
|
+
end
|
117
|
+
|
118
|
+
it 'allows a Method object to be retrieved' do
|
119
|
+
expect(Eventually.new { |success| success.call(value) }.method(:challenge)).to be_a(Method)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
context 'when the value doesn’t respond to the message' do
|
124
|
+
it 'reports that the Eventually doesn’t respond to the message' do
|
125
|
+
expect(Eventually.new { |success| success.call(double) }).not_to respond_to(:challenge)
|
126
|
+
end
|
127
|
+
|
128
|
+
it 'doesn’t allow a Method object to be retrieved' do
|
129
|
+
expect { Eventually.new { |success| success.call(double) }.method(:challenge) }.to raise_error(NameError)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
context 'when value is Enumerable' do
|
134
|
+
let(:value) { [1, 2, 3] }
|
135
|
+
|
136
|
+
it 'forwards any unrecognised message to the value' do
|
137
|
+
expect(Eventually.new { |success| success.call(value) }.first).to be_a(Eventually)
|
138
|
+
expect(Eventually.new { |success| success.call(value) }.first.run { |value| value }).to eq 1
|
139
|
+
expect(Eventually.new { |success| success.call(value) }.last).to be_a(Eventually)
|
140
|
+
expect(Eventually.new { |success| success.call(value) }.last.run { |value| value }).to eq 3
|
141
|
+
end
|
142
|
+
end
|
110
143
|
end
|
111
144
|
end
|
112
145
|
end
|
data/spec/monads/many_spec.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
1
3
|
require 'monads/many'
|
2
4
|
|
3
5
|
module Monads
|
@@ -90,6 +92,39 @@ module Monads
|
|
90
92
|
it 'returns the messages’ results wrapped in a Many' do
|
91
93
|
expect(many.challenge.values).to eq responses
|
92
94
|
end
|
95
|
+
|
96
|
+
context 'when all of the values respond to the message' do
|
97
|
+
it 'reports that the Many responds to the message' do
|
98
|
+
expect(many).to respond_to(:challenge)
|
99
|
+
end
|
100
|
+
|
101
|
+
it 'allows a Method object to be retrieved' do
|
102
|
+
expect(many.method(:challenge)).to be_a(Method)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
context 'when any of the values don’t respond to the message' do
|
107
|
+
let(:many) { Many.new(values + [double]) }
|
108
|
+
|
109
|
+
it 'reports that the Many doesn’t respond to the message' do
|
110
|
+
expect(many).not_to respond_to(:challenge)
|
111
|
+
end
|
112
|
+
|
113
|
+
it 'doesn’t allow a Method object to be retrieved' do
|
114
|
+
expect { many.method(:challenge) }.to raise_error(NameError)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
context 'when values are Enumerable' do
|
119
|
+
let(:values) { [[1, 2], [3, 5]] }
|
120
|
+
|
121
|
+
it 'forwards any unrecognised message to the value' do
|
122
|
+
expect(many.first).to be_a(Many)
|
123
|
+
expect(many.first.values).to eq [1, 3]
|
124
|
+
expect(many.last).to be_a(Many)
|
125
|
+
expect(many.last.values).to eq [2, 5]
|
126
|
+
end
|
127
|
+
end
|
93
128
|
end
|
94
129
|
end
|
95
130
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
1
3
|
require 'monads/optional'
|
2
4
|
|
3
5
|
module Monads
|
@@ -92,6 +94,39 @@ module Monads
|
|
92
94
|
it 'returns the message’s result wrapped in an Optional' do
|
93
95
|
expect(optional.challenge.value).to eq response
|
94
96
|
end
|
97
|
+
|
98
|
+
context 'when the value responds to the message' do
|
99
|
+
it 'reports that the Optional responds to the message' do
|
100
|
+
expect(optional).to respond_to(:challenge)
|
101
|
+
end
|
102
|
+
|
103
|
+
it 'allows a Method object to be retrieved' do
|
104
|
+
expect(optional.method(:challenge)).to be_a(Method)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
context 'when the value doesn’t respond to the message' do
|
109
|
+
let(:optional) { Optional.new(double) }
|
110
|
+
|
111
|
+
it 'reports that the Optional doesn’t respond to the message' do
|
112
|
+
expect(optional).not_to respond_to(:challenge)
|
113
|
+
end
|
114
|
+
|
115
|
+
it 'doesn’t allow a Method object to be retrieved' do
|
116
|
+
expect { optional.method(:challenge) }.to raise_error(NameError)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
context 'when value is Enumerable' do
|
121
|
+
let(:value) { [1, 2, 3] }
|
122
|
+
|
123
|
+
it 'forwards any unrecognised message to the value' do
|
124
|
+
expect(optional.first).to be_a(Optional)
|
125
|
+
expect(optional.first.value).to eq 1
|
126
|
+
expect(optional.last).to be_a(Optional)
|
127
|
+
expect(optional.last.value).to eq 3
|
128
|
+
end
|
129
|
+
end
|
95
130
|
end
|
96
131
|
end
|
97
132
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: monads
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tom Stuart
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2016-02-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rspec
|
@@ -38,8 +38,10 @@ extra_rdoc_files: []
|
|
38
38
|
files:
|
39
39
|
- ".gitignore"
|
40
40
|
- ".rspec"
|
41
|
+
- ".travis.yml"
|
41
42
|
- Gemfile
|
42
43
|
- LICENSE.txt
|
44
|
+
- README.md
|
43
45
|
- lib/monads.rb
|
44
46
|
- lib/monads/eventually.rb
|
45
47
|
- lib/monads/many.rb
|
@@ -71,7 +73,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
71
73
|
version: '0'
|
72
74
|
requirements: []
|
73
75
|
rubyforge_project:
|
74
|
-
rubygems_version: 2.
|
76
|
+
rubygems_version: 2.5.1
|
75
77
|
signing_key:
|
76
78
|
specification_version: 4
|
77
79
|
summary: Simple Ruby implementations of some common monads.
|