monads 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 992bed3108c9bd2b84cef78ba21bd99f56f52b88
4
+ data.tar.gz: 58c6f72fcf8c4c29caf1311462bc448c84f510aa
5
+ SHA512:
6
+ metadata.gz: 784a2a5bf9c923501d4f20c41c955ebd6d0b0252574c02714e1c0811d197a5fc0d51d87bef0e731982a56c26277eb7f180230714e8779164162b93b2922b477c
7
+ data.tar.gz: 83c250b59b392cb1f13613cac33dd4a80a5307a65be72f0251ed021c3b7205f8b6f81f3cb9d816ec300dff60c68df0b60af91e506a77f388df22b13cdcefa289
@@ -0,0 +1 @@
1
+ Gemfile.lock
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --color
2
+ --warnings
3
+ --require spec_helper
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org/'
2
+
3
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Tom Stuart
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.
@@ -0,0 +1,4 @@
1
+ require 'monads/eventually'
2
+ require 'monads/many'
3
+ require 'monads/optional'
4
+ require 'monads/version'
@@ -0,0 +1,31 @@
1
+ require 'monads/monad'
2
+
3
+ module Monads
4
+ Eventually = Struct.new(:block) do
5
+ include Monad
6
+
7
+ def initialize(&block)
8
+ super(block)
9
+ end
10
+
11
+ def run(&success)
12
+ block.call(success)
13
+ end
14
+
15
+ def and_then(&block)
16
+ block = ensure_monadic_result(&block)
17
+
18
+ Eventually.new do |success|
19
+ run do |value|
20
+ block.call(value).run(&success)
21
+ end
22
+ end
23
+ end
24
+
25
+ def self.from_value(value)
26
+ Eventually.new do |success|
27
+ success.call(value)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,17 @@
1
+ require 'monads/monad'
2
+
3
+ module Monads
4
+ Many = Struct.new(:values) do
5
+ include Monad
6
+
7
+ def and_then(&block)
8
+ block = ensure_monadic_result(&block)
9
+
10
+ Many.new(values.map(&block).flat_map(&:values))
11
+ end
12
+
13
+ def self.from_value(value)
14
+ Many.new([value])
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,29 @@
1
+ module Monads
2
+ module Monad
3
+ def within(&block)
4
+ and_then do |value|
5
+ self.class.from_value(block.call(value))
6
+ end
7
+ end
8
+
9
+ def method_missing(*args, &block)
10
+ within do |value|
11
+ value.public_send(*args, &block)
12
+ end
13
+ end
14
+
15
+ private
16
+
17
+ def ensure_monadic_result(&block)
18
+ acceptable_result_type = self.class
19
+
20
+ -> *a, &b do
21
+ block.call(*a, &b).tap do |result|
22
+ unless result.is_a?(acceptable_result_type)
23
+ raise TypeError, "block must return #{acceptable_result_type.name}"
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,21 @@
1
+ require 'monads/monad'
2
+
3
+ module Monads
4
+ Optional = Struct.new(:value) do
5
+ include Monad
6
+
7
+ def and_then(&block)
8
+ block = ensure_monadic_result(&block)
9
+
10
+ if value.nil?
11
+ self
12
+ else
13
+ block.call(value)
14
+ end
15
+ end
16
+
17
+ def self.from_value(value)
18
+ Optional.new(value)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,3 @@
1
+ module Monads
2
+ VERSION = '0.0.1'
3
+ end
@@ -0,0 +1,19 @@
1
+ lib = File.expand_path('../lib', __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require 'monads/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'monads'
7
+ spec.version = Monads::VERSION
8
+ spec.author = 'Tom Stuart'
9
+ spec.email = 'tom@codon.com'
10
+ spec.summary = 'Simple Ruby implementations of some common monads.'
11
+ spec.homepage = 'https://github.com/tomstuart/monads'
12
+ spec.license = 'MIT'
13
+
14
+ spec.files = `git ls-files -z`.split("\x0")
15
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
16
+ spec.require_paths = ['lib']
17
+
18
+ spec.add_development_dependency 'rspec', '~> 3.0', '>= 3.0.0'
19
+ end
@@ -0,0 +1,112 @@
1
+ require 'monads/eventually'
2
+
3
+ module Monads
4
+ RSpec.describe 'the Eventually monad' do
5
+ describe '#run' do
6
+ it 'runs the block from an Eventually' do
7
+ expect { |block| Eventually.new(&block).run }.to yield_control
8
+ end
9
+
10
+ it 'uses its block argument as a success callback' do
11
+ @result, result = nil, double
12
+ Eventually.new { |success| success.call(result) }.run { |value| @result = value }
13
+ expect(@result).to eq result
14
+ end
15
+ end
16
+
17
+ describe '#and_then' do
18
+ context 'when the Eventually succeeds synchronously' do
19
+ it 'arranges for the block to run when the Eventually succeeds' do
20
+ @result, intermediate_result, final_result = nil, double, double
21
+ Eventually.new { |success| success.call(intermediate_result) }.
22
+ and_then do |value|
23
+ if value == intermediate_result
24
+ Eventually.new { |success| success.call(final_result) }
25
+ end
26
+ end.
27
+ run { |value| @result = value }
28
+ expect(@result).to eq final_result
29
+ end
30
+
31
+ it 'raises an error if the block doesn’t return another Eventually' do
32
+ expect { Eventually.new { |success| success.call }.and_then { double }.run }.to raise_error(TypeError)
33
+ end
34
+ end
35
+
36
+ context 'when the Eventually succeds asynchronously' do
37
+ it 'arranges for the block to run when the Eventually succeeds' do
38
+ @result, intermediate_result, final_result = nil, double, double
39
+ Eventually.new { |success| @job = -> { success.call(intermediate_result) } }.
40
+ and_then do |value|
41
+ if value == intermediate_result
42
+ Eventually.new { |success| success.call(final_result) }
43
+ end
44
+ end.
45
+ run { |value| @result = value }
46
+ @job.call
47
+ expect(@result).to eq final_result
48
+ end
49
+
50
+ it 'raises an error if the block doesn’t return another Eventually' do
51
+ expect {
52
+ Eventually.new { |success| @job = -> { success.call } }.and_then { double }.run
53
+ @job.call
54
+ }.to raise_error(TypeError)
55
+ end
56
+ end
57
+ end
58
+
59
+ describe '.from_value' do
60
+ let(:value) { double }
61
+
62
+ it 'wraps a value in an Eventually' do
63
+ @result = nil
64
+ Eventually.from_value(value).run { |value| @result = value }
65
+ expect(@result).to eq value
66
+ end
67
+ end
68
+
69
+ describe '#within' do
70
+ context 'when the Eventually succeeds synchronously' do
71
+ it 'arranges for the block to run when the Eventually succeeds' do
72
+ @result, intermediate_result, final_result = nil, double, double
73
+ Eventually.new { |success| success.call(intermediate_result) }.
74
+ within { |value| final_result if value == intermediate_result }.
75
+ run { |value| @result = value }
76
+ expect(@result).to eq final_result
77
+ end
78
+ end
79
+
80
+ context 'when the Eventually succeds asynchronously' do
81
+ it 'arranges for the block to run when the Eventually succeeds' do
82
+ @result, intermediate_result, final_result = nil, double, double
83
+ Eventually.new { |success| @job = -> { success.call(intermediate_result) } }.
84
+ within { |value| final_result if value == intermediate_result }.
85
+ run { |value| @result = value }
86
+ @job.call
87
+ expect(@result).to eq final_result
88
+ end
89
+ end
90
+ end
91
+
92
+ describe 'handling unrecognised messages' do
93
+ let(:value) { double }
94
+ let(:response) { double }
95
+
96
+ before(:example) do
97
+ allow(value).to receive(:challenge).and_return(response)
98
+ end
99
+
100
+ it 'forwards any unrecognised message to the block’s value' do
101
+ expect(value).to receive(:challenge)
102
+ Eventually.new { |success| success.call(value) }.challenge.run {}
103
+ end
104
+
105
+ it 'returns the message’s result wrapped in an Eventually' do
106
+ @result = nil
107
+ Eventually.new { |success| success.call(value) }.challenge.run { |result| @result = result }
108
+ expect(@result).to eq response
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,95 @@
1
+ require 'monads/many'
2
+
3
+ module Monads
4
+ RSpec.describe 'the Many monad' do
5
+ let(:many) { Many.new(values) }
6
+
7
+ describe '#values' do
8
+ let(:values) { double }
9
+
10
+ it 'retrieves the values from a Many' do
11
+ expect(many.values).to eq values
12
+ end
13
+ end
14
+
15
+ describe '#and_then' do
16
+ context 'when there aren’t any values' do
17
+ let(:values) { [] }
18
+
19
+ it 'doesn’t call the block' do
20
+ expect { |block| many.and_then(&block) }.not_to yield_control
21
+ end
22
+ end
23
+
24
+ context 'when there are values' do
25
+ let(:values) { [1, 2, 3] }
26
+
27
+ it 'calls the block with each value' do
28
+ @values = []
29
+ many.and_then { |value| @values << value; Many.new(double) }
30
+ expect(@values).to eq values
31
+ end
32
+
33
+ it 'returns a flattened version of the block’s results' do
34
+ expect(many.and_then { |value| Many.new(value * 2) }.values).to eq [2, 4, 6]
35
+ end
36
+
37
+ it 'raises an error if the block doesn’t return another Many' do
38
+ expect { many.and_then { double } }.to raise_error(TypeError)
39
+ end
40
+ end
41
+ end
42
+
43
+ describe '.from_value' do
44
+ let(:value) { double }
45
+
46
+ it 'wraps a value in a Many' do
47
+ expect(Many.from_value(value).values).to eq [value]
48
+ end
49
+ end
50
+
51
+ describe '#within' do
52
+ context 'when there aren’t any values' do
53
+ let(:values) { [] }
54
+
55
+ it 'doesn’t call the block' do
56
+ expect { |block| many.within(&block) }.not_to yield_control
57
+ end
58
+ end
59
+
60
+ context 'when there are values' do
61
+ let(:values) { [1, 2, 3] }
62
+
63
+ it 'calls the block with each value' do
64
+ expect { |block| many.within(&block) }.to yield_successive_args(*values)
65
+ end
66
+
67
+ it 'returns a flattened version of the block’s results wrapped in a Many' do
68
+ expect(many.within { |value| value * 2 }.values).to eq [2, 4, 6]
69
+ end
70
+ end
71
+ end
72
+
73
+ describe 'handling unrecognised messages' do
74
+ let(:values) { [double, double, double] }
75
+ let(:responses) { [double, double, double] }
76
+
77
+ before(:example) do
78
+ values.zip(responses) do |value, response|
79
+ allow(value).to receive(:challenge).and_return(response)
80
+ end
81
+ end
82
+
83
+ it 'forwards any unrecognised message to each value' do
84
+ values.each do |value|
85
+ expect(value).to receive(:challenge)
86
+ end
87
+ many.challenge
88
+ end
89
+
90
+ it 'returns the messages’ results wrapped in a Many' do
91
+ expect(many.challenge.values).to eq responses
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,97 @@
1
+ require 'monads/optional'
2
+
3
+ module Monads
4
+ RSpec.describe 'the Optional monad' do
5
+ let(:value) { double }
6
+ let(:optional) { Optional.new(value) }
7
+
8
+ describe '#value' do
9
+ it 'retrieves the value from an Optional' do
10
+ expect(optional.value).to eq value
11
+ end
12
+ end
13
+
14
+ describe '#and_then' do
15
+ context 'when the value is nil' do
16
+ before(:example) do
17
+ allow(value).to receive(:nil?).and_return(true)
18
+ end
19
+
20
+ it 'doesn’t call the block' do
21
+ expect { |block| optional.and_then(&block) }.not_to yield_control
22
+ end
23
+ end
24
+
25
+ context 'when the value isn’t nil' do
26
+ before(:example) do
27
+ allow(value).to receive(:nil?).and_return(false)
28
+ end
29
+
30
+ it 'calls the block with the value' do
31
+ @value = nil
32
+ optional.and_then { |value| @value = value; Optional.new(double) }
33
+ expect(@value).to eq value
34
+ end
35
+
36
+ it 'returns the block’s result' do
37
+ result = double
38
+ expect(optional.and_then { |value| Optional.new(result) }.value).to eq result
39
+ end
40
+
41
+ it 'raises an error if the block doesn’t return another Optional' do
42
+ expect { optional.and_then { double } }.to raise_error(TypeError)
43
+ end
44
+ end
45
+ end
46
+
47
+ describe '.from_value' do
48
+ it 'wraps a value in an Optional' do
49
+ expect(Optional.from_value(value).value).to eq value
50
+ end
51
+ end
52
+
53
+ describe '#within' do
54
+ context 'when the value is nil' do
55
+ before(:example) do
56
+ allow(value).to receive(:nil?).and_return(true)
57
+ end
58
+
59
+ it 'doesn’t call the block' do
60
+ expect { |block| optional.within(&block) }.not_to yield_control
61
+ end
62
+ end
63
+
64
+ context 'when the value isn’t nil' do
65
+ before(:example) do
66
+ allow(value).to receive(:nil?).and_return(false)
67
+ end
68
+
69
+ it 'calls the block with the value' do
70
+ expect { |block| optional.within(&block) }.to yield_with_args(value)
71
+ end
72
+
73
+ it 'returns the block’s result wrapped in an Optional' do
74
+ result = double
75
+ expect(optional.within { result }.value).to eq result
76
+ end
77
+ end
78
+ end
79
+
80
+ describe 'handling unrecognised messages' do
81
+ let(:response) { double }
82
+
83
+ before(:example) do
84
+ allow(value).to receive(:challenge).and_return(response)
85
+ end
86
+
87
+ it 'forwards any unrecognised message to the value' do
88
+ expect(value).to receive(:challenge)
89
+ optional.challenge
90
+ end
91
+
92
+ it 'returns the message’s result wrapped in an Optional' do
93
+ expect(optional.challenge.value).to eq response
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,3 @@
1
+ RSpec.configure do |config|
2
+ config.disable_monkey_patching!
3
+ end
metadata ADDED
@@ -0,0 +1,82 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: monads
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Tom Stuart
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-08-02 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rspec
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '3.0'
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 3.0.0
23
+ type: :development
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: '3.0'
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 3.0.0
33
+ description:
34
+ email: tom@codon.com
35
+ executables: []
36
+ extensions: []
37
+ extra_rdoc_files: []
38
+ files:
39
+ - ".gitignore"
40
+ - ".rspec"
41
+ - Gemfile
42
+ - LICENSE.txt
43
+ - lib/monads.rb
44
+ - lib/monads/eventually.rb
45
+ - lib/monads/many.rb
46
+ - lib/monads/monad.rb
47
+ - lib/monads/optional.rb
48
+ - lib/monads/version.rb
49
+ - monads.gemspec
50
+ - spec/monads/eventually_spec.rb
51
+ - spec/monads/many_spec.rb
52
+ - spec/monads/optional_spec.rb
53
+ - spec/spec_helper.rb
54
+ homepage: https://github.com/tomstuart/monads
55
+ licenses:
56
+ - MIT
57
+ metadata: {}
58
+ post_install_message:
59
+ rdoc_options: []
60
+ require_paths:
61
+ - lib
62
+ required_ruby_version: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: '0'
67
+ required_rubygems_version: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: '0'
72
+ requirements: []
73
+ rubyforge_project:
74
+ rubygems_version: 2.2.2
75
+ signing_key:
76
+ specification_version: 4
77
+ summary: Simple Ruby implementations of some common monads.
78
+ test_files:
79
+ - spec/monads/eventually_spec.rb
80
+ - spec/monads/many_spec.rb
81
+ - spec/monads/optional_spec.rb
82
+ - spec/spec_helper.rb