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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 992bed3108c9bd2b84cef78ba21bd99f56f52b88
4
- data.tar.gz: 58c6f72fcf8c4c29caf1311462bc448c84f510aa
3
+ metadata.gz: 87404c6f7a69fed0181330dcaefafe2760574f36
4
+ data.tar.gz: 210ed8c40e603d2e0e9d13151170c37ab83443c2
5
5
  SHA512:
6
- metadata.gz: 784a2a5bf9c923501d4f20c41c955ebd6d0b0252574c02714e1c0811d197a5fc0d51d87bef0e731982a56c26277eb7f180230714e8779164162b93b2922b477c
7
- data.tar.gz: 83c250b59b392cb1f13613cac33dd4a80a5307a65be72f0251ed021c3b7205f8b6f81f3cb9d816ec300dff60c68df0b60af91e506a77f388df22b13cdcefa289
6
+ metadata.gz: 42898713212d672e5a774693ea259b1a62c2a274442cf141ab8bc1c97a85a7d8892589e62a480d5d437b7e6f78bfe22f60bceaa5bc50ff1b67ecc2bc9ce4989f
7
+ data.tar.gz: 43306a7166f3a1a653d2dfb8461bde9d8b4583a0c6ce08bba3b506147e5ebeecac4e5a41868a6d201e101fea4d21cab2ff9b679983cd6a50e23b80688bfef07c
@@ -0,0 +1,11 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.2
4
+ - 1.9.3
5
+ - 2.0.0
6
+ - 2.1.8
7
+ - 2.2.4
8
+ - 2.3.0
9
+ script: bundle exec rspec
10
+ before_install:
11
+ - gem install bundler
@@ -0,0 +1,98 @@
1
+ # Monads
2
+
3
+ [![Build Status](https://travis-ci.org/tomstuart/monads.svg?branch=master)](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
+ ```
@@ -1,11 +1,13 @@
1
1
  require 'monads/monad'
2
2
 
3
3
  module Monads
4
- Eventually = Struct.new(:block) do
4
+ class Eventually
5
5
  include Monad
6
6
 
7
+ attr_reader :block
8
+
7
9
  def initialize(&block)
8
- super(block)
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)
@@ -1,15 +1,25 @@
1
1
  require 'monads/monad'
2
2
 
3
3
  module Monads
4
- Many = Struct.new(:values) do
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
@@ -17,7 +17,7 @@ module Monads
17
17
  def ensure_monadic_result(&block)
18
18
  acceptable_result_type = self.class
19
19
 
20
- -> *a, &b do
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}"
@@ -1,9 +1,15 @@
1
1
  require 'monads/monad'
2
2
 
3
3
  module Monads
4
- Optional = Struct.new(:value) do
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
@@ -1,3 +1,3 @@
1
1
  module Monads
2
- VERSION = '0.0.1'
2
+ VERSION = '0.1.0'
3
3
  end
@@ -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
@@ -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.1
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: 2014-08-02 00:00:00.000000000 Z
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.2.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.