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 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 equivalend in Ruby](http://stackoverflow.com/questions/2709361/monad-equivalent-in-ruby)
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
 
@@ -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 :>= :bind
39
- alias :+ :bind
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
 
@@ -1,4 +1,7 @@
1
1
  module Monadic
2
2
  # thrown by Option#fetch if it has no value and no default was provided
3
3
  class NoValueError < StandardError; end
4
+
5
+ # thrown when a Success or Failure was expected, but anything else was returned
6
+ class NotEitherError < StandardError; end
4
7
  end
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Monadic
2
- VERSION = "0.0.4"
2
+ VERSION = "0.0.5"
3
3
  end
data/lib/monadic.rb CHANGED
@@ -3,6 +3,7 @@ require 'monadic/version'
3
3
  require 'monadic/errors'
4
4
  require 'monadic/option'
5
5
  require 'monadic/either'
6
+ require 'monadic/validation'
6
7
 
7
8
  module Monadic
8
9
  end
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
- chain -> { User.find(-1) }
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
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-01 00:00:00.000000000 Z
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