monadic 0.0.4 → 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +15 -0
- data/README.md +65 -10
- data/lib/monadic/either.rb +3 -7
- data/lib/monadic/errors.rb +3 -0
- data/lib/monadic/validation.rb +24 -0
- data/lib/monadic/version.rb +1 -1
- data/lib/monadic.rb +1 -0
- data/spec/either_spec.rb +13 -1
- data/spec/validation_spec.rb +47 -0
- metadata +5 -2
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,20 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## v0.0.5
|
4
|
+
|
5
|
+
Removed the `#chain` method alias for bind in `Either`.
|
6
|
+
|
7
|
+
Moar examples with instance variables in an `Either.chain`.
|
8
|
+
|
9
|
+
Add monadic `Validation`, which is a special application (an applicative functor) of the Either Monad e.g.
|
10
|
+
|
11
|
+
Validation() do
|
12
|
+
check { check_age.(person.age); }
|
13
|
+
check { check_sobriety.(person.sobriety) }
|
14
|
+
end
|
15
|
+
|
16
|
+
It returns a Success([]) with an empty list, or a Failure([..]) with a list of all Failures.
|
17
|
+
|
3
18
|
## v0.0.4
|
4
19
|
|
5
20
|
To be more idiomatic rubyuesque, rename `#value` to `#fetch`, which throws now an `NoValueError`.
|
data/README.md
CHANGED
@@ -1,17 +1,22 @@
|
|
1
1
|
# Monadic
|
2
2
|
|
3
|
-
helps dealing with exceptional situations, it comes from the sphere of functional programming and bringing the goodies I have come to love in Scala to my ruby projects (hence I will be using more Scala like constructs than Haskell).
|
3
|
+
helps dealing with exceptional situations, it comes from the sphere of functional programming and bringing the goodies I have come to love in [Scala](http://www.scala-lang.org/) to my ruby projects (hence I will be using more [Scala](http://www.scala-lang.org/) like constructs than Haskell).
|
4
4
|
|
5
5
|
My motivation to create this gem was that I often work with nested Hashes and need to reach deeply inside of them so my code is sprinkled with things like some_hash.fetch(:one, {}).fetch(:two, {}).fetch(:three, "unknown").
|
6
6
|
|
7
|
-
We have the following monadics:
|
7
|
+
We have the following monadics (monads, functors, applicatives and variations):
|
8
8
|
|
9
|
-
- Option (Maybe in Haskell) - Scala like with a rubyesque flavour
|
9
|
+
- Option (Maybe in Haskell) - [Scala](http://www.scala-lang.org/) like with a rubyesque flavour
|
10
10
|
- Either - more Haskell like
|
11
|
+
- Validation
|
11
12
|
|
12
13
|
What's the point of using monads in ruby? To me it started with having a safe way to deal with nil objects and other exceptions.
|
13
14
|
Thus you contain the erroneous behaviour within a monad - an indivisible, impenetrable unit.
|
14
15
|
|
16
|
+
Monad purists might complain that there is no unit method to get the zero monad, I didn't include them, as I didn't find this idiomatic to the ruby language. I prefer to focus on the pragmatic uses of monads. If you want to learn moar about monads, see the references section at the bottom.
|
17
|
+
|
18
|
+
A monad is most effectively described as a computation that eventually returns a value. -- Wolfgang De Meuter
|
19
|
+
|
15
20
|
## Usage
|
16
21
|
|
17
22
|
### Option
|
@@ -105,14 +110,15 @@ Slug example
|
|
105
110
|
|
106
111
|
### Either
|
107
112
|
Its main purpose here to handle errors gracefully, by chaining multiple calls in a functional way and stop evaluating them as soon as the first fails.
|
108
|
-
Assume you need several calls to construct some object in order to be useful, after each you need to check for success. Also you want to catch exceptions and not let them bubble upwards.
|
113
|
+
Assume you need several calls to construct some object in order to be useful, after each you need to check for success. Also you want to catch exceptions and not let them bubble upwards.
|
114
|
+
What is specific to this implementation is that exceptions are caught within the execution blocks. This way I have all error conditions wrapped in one place.
|
109
115
|
|
110
116
|
`Success` represents a successfull execution of an operation (Right in Scala, Haskell).
|
111
117
|
`Failure` represents a failure to execute an operation (Left in Scala, Haskell).
|
112
118
|
|
113
|
-
The `Either()` wrapper will treat `nil`, `false` or `empty?` as a `Failure` and all others as `Success`.
|
119
|
+
The `Either()` wrapper will treat all falsey values `nil`, `false` or `empty?` as a `Failure` and all others as `Success`. If that does not suit you, use `Success` or `Failure` only.
|
114
120
|
|
115
|
-
result = parse_and_validate_params(params).
|
121
|
+
result = parse_and_validate_params(params). # must return a Success or Failure inside
|
116
122
|
bind ->(user_id) { User.find(user_id) }. # if #find returns null it will become a Failure
|
117
123
|
bind ->(user) { authorized?(user); user }. # if authorized? raises an Exception, it will be a Failure
|
118
124
|
bind ->(user) { UserDecorator(user) }
|
@@ -154,15 +160,62 @@ Exceptions are wrapped into a Failure:
|
|
154
160
|
Another example:
|
155
161
|
|
156
162
|
Success(params).
|
157
|
-
bind ->(params) { Either(params.fetch(:path)) }
|
158
|
-
bind ->(path) { load_stuff(params)
|
163
|
+
bind ->(params) { Either(params.fetch(:path)) } # fails if params does not contain :path
|
164
|
+
bind ->(path) { load_stuff(params) } #
|
165
|
+
|
166
|
+
Storing intermediate results in instance variables is possible, although it is not very elegant:
|
167
|
+
|
168
|
+
result = Either.chain do
|
169
|
+
bind { @map = { one: 1, two: 2 } }
|
170
|
+
bind { @map.fetch(:one) }
|
171
|
+
bind { |p| Success(p + 100) }
|
172
|
+
end
|
173
|
+
|
174
|
+
result == Success(101)
|
175
|
+
|
176
|
+
|
177
|
+
### Validation
|
178
|
+
The Validation applicative functor, takes a list of checks within a block. Each check must return either Success of Failure.
|
179
|
+
If Successful, it will return Success, if not a Failure monad, containing a list of failures.
|
180
|
+
Within the Failure() provide the reason why the check failed.
|
181
|
+
|
182
|
+
Example:
|
183
|
+
|
184
|
+
def validate(person)
|
185
|
+
check_age = ->(age_expr) {
|
186
|
+
age = age_expr.to_i
|
187
|
+
case
|
188
|
+
when age <= 0; Failure('Age must be > 0')
|
189
|
+
when age > 130; Failure('Age must be < 130')
|
190
|
+
else Success(age)
|
191
|
+
end
|
192
|
+
}
|
193
|
+
|
194
|
+
check_sobriety = ->(sobriety) {
|
195
|
+
case sobriety
|
196
|
+
when :sober, :tipsy; Success(sobriety)
|
197
|
+
when :drunk ; Failure('No drunks allowed')
|
198
|
+
else Failure("Sobriety state '#{sobriety}' is not allowed")
|
199
|
+
end
|
200
|
+
}
|
201
|
+
|
202
|
+
check_gender = ->(gender) {
|
203
|
+
gender == :male || gender == :female ? Success(gender) : Failure("Invalid gender #{gender}")
|
204
|
+
}
|
205
|
+
|
206
|
+
Validation() do
|
207
|
+
check { check_age.(person.age); }
|
208
|
+
check { check_sobriety.(person.sobriety) }
|
209
|
+
check { check_gender.(person.gender) }
|
210
|
+
end
|
211
|
+
end
|
159
212
|
|
160
213
|
|
161
214
|
## References
|
162
215
|
|
163
216
|
* [Wikipedia Monad](See also http://en.wikipedia.org/wiki/Monad)
|
164
217
|
* [Learn You a Haskell - for a few monads more](http://learnyouahaskell.com/for-a-few-monads-more)
|
165
|
-
* [Monad
|
218
|
+
* [Monad equivalent in Ruby](http://stackoverflow.com/questions/2709361/monad-equivalent-in-ruby)
|
166
219
|
* [Option Type ](http://devblog.avdi.org/2011/05/30/null-objects-and-falsiness/)
|
167
220
|
* [NullObject and Falsiness by @avdi](http://devblog.avdi.org/2011/05/30/null-objects-and-falsiness/)
|
168
221
|
* [andand](https://github.com/raganwald/andand/blob/master/README.textile)
|
@@ -172,8 +225,10 @@ Another example:
|
|
172
225
|
* [Monads in Ruby with nice syntax](http://www.valuedlessons.com/2008/01/monads-in-ruby-with-nice-syntax.html)
|
173
226
|
* [Maybe in Ruby](https://github.com/bhb/maybe)
|
174
227
|
* [Monads on the Cheap](http://osteele.com/archives/2007/12/cheap-monads)
|
175
|
-
* [Rumonade](https://github.com/ms-ati/rumonade)
|
228
|
+
* [Rumonade - another excellent (more scala-like) monad implementation](https://github.com/ms-ati/rumonade)
|
176
229
|
* [Monads for functional programming](http://homepages.inf.ed.ac.uk/wadler/papers/marktoberdorf/baastad.pdf)
|
230
|
+
* [Monads as a theoretical foundation for AOP](http://soft.vub.ac.be/Publications/1997/vub-prog-tr-97-10.pdf)
|
231
|
+
* [What is an applicative functor?](http://applicative-errors-scala.googlecode.com/svn/artifacts/0.6/html/index.html)
|
177
232
|
|
178
233
|
## Installation
|
179
234
|
|
data/lib/monadic/either.rb
CHANGED
@@ -1,7 +1,5 @@
|
|
1
|
-
# Chain various method calls
|
1
|
+
# Chain various method calls
|
2
2
|
module Either
|
3
|
-
class NoEitherError < StandardError; end
|
4
|
-
|
5
3
|
def self.chain(initial=nil, &block)
|
6
4
|
Either::Chain.new(&block).call(initial)
|
7
5
|
end
|
@@ -35,9 +33,8 @@ module Either
|
|
35
33
|
Failure(ex)
|
36
34
|
end
|
37
35
|
end
|
38
|
-
alias :>=
|
39
|
-
alias :+
|
40
|
-
alias :chain :bind
|
36
|
+
alias :>= :bind
|
37
|
+
alias :+ :bind
|
41
38
|
|
42
39
|
def to_s
|
43
40
|
"#{self.class.name}(#{@value.nil? ? 'nil' : @value.to_s})"
|
@@ -65,7 +62,6 @@ class Either::Chain
|
|
65
62
|
def bind(proc=nil, &block)
|
66
63
|
@chain << (proc || block)
|
67
64
|
end
|
68
|
-
alias :chain :bind
|
69
65
|
|
70
66
|
end
|
71
67
|
|
data/lib/monadic/errors.rb
CHANGED
@@ -0,0 +1,24 @@
|
|
1
|
+
def Validation(&block)
|
2
|
+
Validation.new.call(&block)
|
3
|
+
end
|
4
|
+
|
5
|
+
# Conducts several function calls which do checks, of which each must return Success or Failure
|
6
|
+
# and returns a list of all failures.
|
7
|
+
# Validation is not a monad, but an deemed an applicative functor
|
8
|
+
class Validation
|
9
|
+
def initialize
|
10
|
+
@result = Success([])
|
11
|
+
end
|
12
|
+
|
13
|
+
def call(&block)
|
14
|
+
instance_eval(&block)
|
15
|
+
end
|
16
|
+
|
17
|
+
def check(proc=nil, &block)
|
18
|
+
result = (proc || block).call
|
19
|
+
raise NotEitherError, "Expected #{result.inspect} to be an Either" unless result.is_a? Either
|
20
|
+
@result = Failure(@result.fetch << result.fetch) if result.failure?
|
21
|
+
|
22
|
+
@result
|
23
|
+
end
|
24
|
+
end
|
data/lib/monadic/version.rb
CHANGED
data/lib/monadic.rb
CHANGED
data/spec/either_spec.rb
CHANGED
@@ -34,6 +34,7 @@ describe 'Either' do
|
|
34
34
|
end
|
35
35
|
|
36
36
|
class User
|
37
|
+
attr :age, :gender, :sobriety
|
37
38
|
def self.find(id)
|
38
39
|
case id
|
39
40
|
when -1; raise 'invalid user id'
|
@@ -62,7 +63,7 @@ describe 'Either' do
|
|
62
63
|
|
63
64
|
it 'works' do
|
64
65
|
either = Either(true).
|
65
|
-
|
66
|
+
bind -> { User.find(-1) }
|
66
67
|
either.failure?.should be_true
|
67
68
|
RuntimeError.should === either.fetch
|
68
69
|
end
|
@@ -191,4 +192,15 @@ describe 'Either' do
|
|
191
192
|
either.should be_a_failure
|
192
193
|
KeyError.should === either.fetch
|
193
194
|
end
|
195
|
+
|
196
|
+
it 'instance variables' do
|
197
|
+
result = Either.chain do
|
198
|
+
bind { @map = { one: 1, two: 2 } }
|
199
|
+
bind { Success(100) }
|
200
|
+
bind { @map.fetch(:one) }
|
201
|
+
bind { |p| Success(p + 100) }
|
202
|
+
end
|
203
|
+
|
204
|
+
result.should == Success(101)
|
205
|
+
end
|
194
206
|
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'Validation' do
|
4
|
+
Person = Struct.new(:age, :gender, :sobriety, :name)
|
5
|
+
|
6
|
+
it 'a projection of Success and Failure' do
|
7
|
+
|
8
|
+
def validate(person)
|
9
|
+
check_age = ->(age_expr) {
|
10
|
+
age = age_expr.to_i
|
11
|
+
case
|
12
|
+
when age <= 0; Failure('Age must be > 0')
|
13
|
+
when age > 130; Failure('Age must be < 130')
|
14
|
+
else Success(age)
|
15
|
+
end
|
16
|
+
}
|
17
|
+
|
18
|
+
check_sobriety = ->(sobriety) {
|
19
|
+
case sobriety
|
20
|
+
when :sober, :tipsy; Success(sobriety)
|
21
|
+
when :drunk ; Failure('No drunks allowed')
|
22
|
+
else Failure("Sobriety state '#{sobriety}' is not allowed")
|
23
|
+
end
|
24
|
+
}
|
25
|
+
|
26
|
+
check_gender = ->(gender) {
|
27
|
+
gender == :male || gender == :female ? Success(gender) : Failure("Invalid gender #{gender}")
|
28
|
+
}
|
29
|
+
|
30
|
+
Validation() do
|
31
|
+
check { check_age.(person.age); }
|
32
|
+
check { check_sobriety.(person.sobriety) }
|
33
|
+
check { check_gender.(person.gender) }
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
failure = validate(Person.new(age = 's', gender = :male, sobriety = :drunk, name = 'test'))
|
38
|
+
failure.should be_a Failure
|
39
|
+
failure.success?.should be_false
|
40
|
+
failure.failure?.should be_true
|
41
|
+
failure.should == Failure(["Age must be > 0", "No drunks allowed"])
|
42
|
+
|
43
|
+
success = validate(Person.new(age = 30, gender = :male, sobriety = :sober, name = 'test'))
|
44
|
+
success.should be_a Success
|
45
|
+
success.should == Success([])
|
46
|
+
end
|
47
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: monadic
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.5
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-05-
|
12
|
+
date: 2012-05-02 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rspec
|
@@ -126,11 +126,13 @@ files:
|
|
126
126
|
- lib/monadic/either.rb
|
127
127
|
- lib/monadic/errors.rb
|
128
128
|
- lib/monadic/option.rb
|
129
|
+
- lib/monadic/validation.rb
|
129
130
|
- lib/monadic/version.rb
|
130
131
|
- monadic.gemspec
|
131
132
|
- spec/either_spec.rb
|
132
133
|
- spec/option_spec.rb
|
133
134
|
- spec/spec_helper.rb
|
135
|
+
- spec/validation_spec.rb
|
134
136
|
homepage: http://github.com/pzol/monadic
|
135
137
|
licenses: []
|
136
138
|
post_install_message:
|
@@ -159,3 +161,4 @@ test_files:
|
|
159
161
|
- spec/either_spec.rb
|
160
162
|
- spec/option_spec.rb
|
161
163
|
- spec/spec_helper.rb
|
164
|
+
- spec/validation_spec.rb
|