monads 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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.