kleisli-validation 0.0.1
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.
- data/.gitignore +14 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +76 -0
- data/Rakefile +11 -0
- data/kleisli-validation.gemspec +27 -0
- data/lib/kleisli/semigroup_instances.rb +21 -0
- data/lib/kleisli/validation/version.rb +5 -0
- data/lib/kleisli/validation.rb +112 -0
- data/test/kleisli/validation_test.rb +73 -0
- data/test/test_helper.rb +4 -0
- metadata +126 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2015 Brian Zeligson
|
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.
|
data/README.md
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
# Kleisli::Validation
|
2
|
+
|
3
|
+
A Validation monad built on top of the nice [Kleisli](https://github.com/txus/kleisli)
|
4
|
+
gem.
|
5
|
+
|
6
|
+
Validation is like an Either, but it allows you to accumulate Failures of the
|
7
|
+
same Semigroup using the applicative functor's apply operator.
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
Add this line to your application's Gemfile:
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
gem 'kleisli-validation'
|
15
|
+
```
|
16
|
+
|
17
|
+
And then execute:
|
18
|
+
|
19
|
+
$ bundle
|
20
|
+
|
21
|
+
Or install it yourself as:
|
22
|
+
|
23
|
+
$ gem install kleisli-validation
|
24
|
+
|
25
|
+
## Usage
|
26
|
+
|
27
|
+
Usage is practically identical to the [Either](https://github.com/txus/kleisli#either)
|
28
|
+
Monad provided by kleisli, substituting Success for Right (and .success for .right)
|
29
|
+
as well as Failure for Left (and .failure for .left).
|
30
|
+
|
31
|
+
The main difference is in the handling of errors when using the applicative
|
32
|
+
functor's apply, as seen here.
|
33
|
+
|
34
|
+
```ruby
|
35
|
+
require "kleisli-validation"
|
36
|
+
|
37
|
+
add = -> x, y { x + y }
|
38
|
+
Success(add) * Success(10) * Success(2)
|
39
|
+
# => Success(12)
|
40
|
+
Success(add) * Failure("error") * Success(2)
|
41
|
+
# => Failure("error")
|
42
|
+
|
43
|
+
# new case supported by Validation
|
44
|
+
add3 = -> x, y, z { x + y + z }
|
45
|
+
Success(add3) * Failure(["error"]) * Success(2) * Failure(["another error"])
|
46
|
+
# => Failure(["error", "another error"])
|
47
|
+
|
48
|
+
Success(add3) * Failure(x: "No good") * Success(2) * Failure(x: "Still no good", y: "Also no good")
|
49
|
+
# => Failure(x: ["No good", "Still no good"], y: "Also no good")
|
50
|
+
```
|
51
|
+
|
52
|
+
|
53
|
+
###Semigroups
|
54
|
+
|
55
|
+
The values inside your Failures must all belong to the same Semigroup in
|
56
|
+
order to accumulate errors using the apply operator.
|
57
|
+
|
58
|
+
A Semigroup is any class that implements the sappend method as follows:
|
59
|
+
|
60
|
+
```haskell
|
61
|
+
sappend :: a -> a -> a
|
62
|
+
```
|
63
|
+
|
64
|
+
This gem provides a Semigroup monkeypatch for Array and Hash, see
|
65
|
+
semigroup\_instances.rb for examples and implementation.
|
66
|
+
|
67
|
+
Using Failure values with no sappend implementation or Failure values of
|
68
|
+
differing types in one application will yield an ArgumentError.
|
69
|
+
|
70
|
+
## Contributing
|
71
|
+
|
72
|
+
1. Fork it ( https://github.com/[my-github-username]/kleisli-validation/fork )
|
73
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
74
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
75
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
76
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'kleisli/validation'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "kleisli-validation"
|
8
|
+
spec.version = Kleisli::Validation::VERSION
|
9
|
+
spec.authors = ["Brian Zeligson"]
|
10
|
+
spec.email = ["brian.zeligson@gmail.com"]
|
11
|
+
spec.summary = %q{Validation Monad for Kleisli gem}
|
12
|
+
spec.description = %q{Validation is an Either which can accumulate
|
13
|
+
failures when used with the apply operator (*)
|
14
|
+
and a Semigroup on the Left.}
|
15
|
+
spec.homepage = ""
|
16
|
+
spec.license = "MIT"
|
17
|
+
|
18
|
+
spec.files = `git ls-files -z`.split("\x0")
|
19
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
20
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
21
|
+
spec.require_paths = ["lib"]
|
22
|
+
|
23
|
+
spec.add_development_dependency "bundler", "~> 1.7"
|
24
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
25
|
+
spec.add_development_dependency "minitest", "~> 5.5"
|
26
|
+
spec.add_dependency "kleisli", "~> 0.2.6"
|
27
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require "kleisli"
|
2
|
+
|
3
|
+
# monkey patch some default semigroup instances
|
4
|
+
|
5
|
+
class Array
|
6
|
+
|
7
|
+
def sappend(other)
|
8
|
+
concat(other)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class Hash
|
13
|
+
|
14
|
+
def sappend(other)
|
15
|
+
merge(other) do |k, va, vb|
|
16
|
+
va = [va] unless va.kind_of?(Array)
|
17
|
+
vb = [vb] unless vb.kind_of?(Array)
|
18
|
+
va.concat(vb)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
#require "kleisli/validation/version"
|
2
|
+
require "kleisli"
|
3
|
+
require "kleisli/semigroup_instances.rb"
|
4
|
+
|
5
|
+
module Kleisli
|
6
|
+
class Validation < Either
|
7
|
+
VERSION = "0.0.1"
|
8
|
+
attr_accessor :success, :failure
|
9
|
+
|
10
|
+
def *(other)
|
11
|
+
self >-> f {
|
12
|
+
other >-> val {
|
13
|
+
Success(f.arity > 1 ? f.curry.call(val) : f.call(val))
|
14
|
+
}
|
15
|
+
}
|
16
|
+
end
|
17
|
+
|
18
|
+
class Success < Validation
|
19
|
+
alias value success
|
20
|
+
|
21
|
+
def *(other)
|
22
|
+
if other.class == Success
|
23
|
+
super
|
24
|
+
else
|
25
|
+
other
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def initialize(success)
|
30
|
+
@success = success
|
31
|
+
end
|
32
|
+
|
33
|
+
def >(f)
|
34
|
+
f.call(@success)
|
35
|
+
end
|
36
|
+
|
37
|
+
def fmap(&f)
|
38
|
+
Success.new(f.call(@success))
|
39
|
+
end
|
40
|
+
|
41
|
+
def to_maybe
|
42
|
+
Maybe::Some.new(@success)
|
43
|
+
end
|
44
|
+
|
45
|
+
def or(other=nil, &other_blk)
|
46
|
+
self
|
47
|
+
end
|
48
|
+
|
49
|
+
def to_s
|
50
|
+
"Success(#{@success.inspect})"
|
51
|
+
end
|
52
|
+
alias inspect to_s
|
53
|
+
end
|
54
|
+
|
55
|
+
class Failure < Validation
|
56
|
+
alias value failure
|
57
|
+
|
58
|
+
def *(other)
|
59
|
+
if other.class == Failure
|
60
|
+
unless self.failure.class == other.failure.class &&
|
61
|
+
self.failure.respond_to?(:sappend)
|
62
|
+
raise ArgumentError,
|
63
|
+
"Failures must contain members of a common Semigroup"
|
64
|
+
end
|
65
|
+
Failure(self.failure.sappend(other.failure))
|
66
|
+
else
|
67
|
+
self
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def initialize(failure)
|
72
|
+
@failure = failure
|
73
|
+
end
|
74
|
+
|
75
|
+
def >(f)
|
76
|
+
self
|
77
|
+
end
|
78
|
+
|
79
|
+
def fmap(&f)
|
80
|
+
self
|
81
|
+
end
|
82
|
+
|
83
|
+
def to_maybe
|
84
|
+
Maybe::None.new
|
85
|
+
end
|
86
|
+
|
87
|
+
def or(other=self, &other_blk)
|
88
|
+
if other_blk
|
89
|
+
other_blk.call(@failure)
|
90
|
+
else
|
91
|
+
other
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def to_s
|
96
|
+
"Failure(#{@failure.inspect})"
|
97
|
+
end
|
98
|
+
alias inspect to_s
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
Success = Kleisli::Validation::Success.method(:new)
|
104
|
+
Failure = Kleisli::Validation::Failure.method(:new)
|
105
|
+
|
106
|
+
def Success(v)
|
107
|
+
Kleisli::Validation::Success.new(v)
|
108
|
+
end
|
109
|
+
|
110
|
+
def Failure(v)
|
111
|
+
Kleisli::Validation::Failure.new(v)
|
112
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class ValidationTest < Minitest::Test
|
4
|
+
def test_lift_right
|
5
|
+
assert_equal 3, Success(3).value
|
6
|
+
end
|
7
|
+
|
8
|
+
def test_lift_left
|
9
|
+
assert_equal "error", Failure("error").value
|
10
|
+
end
|
11
|
+
|
12
|
+
def test_bind_right
|
13
|
+
v = Success(1) >-> x {
|
14
|
+
if x == 1
|
15
|
+
Success(x + 90)
|
16
|
+
else
|
17
|
+
Failure("FAIL")
|
18
|
+
end
|
19
|
+
}
|
20
|
+
assert_equal Success(91), v
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_bind_left
|
24
|
+
v = Failure("error") >-> x {
|
25
|
+
Success(x * 20)
|
26
|
+
}
|
27
|
+
assert_equal Failure("error"), v
|
28
|
+
end
|
29
|
+
|
30
|
+
def test_fmap_right
|
31
|
+
assert_equal Success(2), Success(1).fmap { |x| x * 2 }
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_fmap_left
|
35
|
+
assert_equal Failure("error"), Failure("error").fmap { |x| x * 2 }
|
36
|
+
end
|
37
|
+
|
38
|
+
def test_to_maybe_right
|
39
|
+
assert_equal Some(2), Success(1).fmap { |x| x * 2 }.to_maybe
|
40
|
+
end
|
41
|
+
|
42
|
+
def test_to_maybe_left
|
43
|
+
assert_equal None(), Failure("error").fmap { |x| x * 2 }.to_maybe
|
44
|
+
end
|
45
|
+
|
46
|
+
def test_pointfree
|
47
|
+
assert_equal Success(10), Success(5) >> F . fn(&Success) . *(2)
|
48
|
+
end
|
49
|
+
|
50
|
+
def test_applicative_functor_right_arity_1
|
51
|
+
assert_equal Success(20), Success(-> x { x * 2 }) * Success(10)
|
52
|
+
end
|
53
|
+
|
54
|
+
def test_applicative_functor_right_arity_2
|
55
|
+
assert_equal Success(20), Success(-> x, y { x * y }) * Success(10) * Success(2)
|
56
|
+
end
|
57
|
+
|
58
|
+
def test_applicative_functor_left
|
59
|
+
assert_equal Failure("error"), Success(-> x, y { x * y }) * Failure("error") * Success(2)
|
60
|
+
end
|
61
|
+
|
62
|
+
def test_accumulates_failures
|
63
|
+
assert_raises(ArgumentError) { Some(-> x, y { x+y }) * Failure(1) * Failure([2]) }
|
64
|
+
|
65
|
+
assert_equal Failure(["error", "another error"]),
|
66
|
+
(Success(-> x, y, z { x * y * z }) * Failure(["error"]) *
|
67
|
+
Success(2) * Failure(["another error"]))
|
68
|
+
|
69
|
+
assert_equal Failure(a: 1, b: [2, 3]),
|
70
|
+
(Success(-> x, y, z { x * y * z }) * Failure(a: 1, b: 2) *
|
71
|
+
Success(2) * Failure(b: 3))
|
72
|
+
end
|
73
|
+
end
|
data/test/test_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,126 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: kleisli-validation
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Brian Zeligson
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2015-02-17 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: bundler
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '1.7'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '1.7'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rake
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ~>
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '10.0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '10.0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: minitest
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ~>
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '5.5'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '5.5'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: kleisli
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ~>
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: 0.2.6
|
70
|
+
type: :runtime
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ~>
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: 0.2.6
|
78
|
+
description: ! "Validation is an Either which can accumulate\n failures
|
79
|
+
when used with the apply operator (*)\n and a Semigroup
|
80
|
+
on the Left."
|
81
|
+
email:
|
82
|
+
- brian.zeligson@gmail.com
|
83
|
+
executables: []
|
84
|
+
extensions: []
|
85
|
+
extra_rdoc_files: []
|
86
|
+
files:
|
87
|
+
- .gitignore
|
88
|
+
- Gemfile
|
89
|
+
- LICENSE.txt
|
90
|
+
- README.md
|
91
|
+
- Rakefile
|
92
|
+
- kleisli-validation.gemspec
|
93
|
+
- lib/kleisli/semigroup_instances.rb
|
94
|
+
- lib/kleisli/validation.rb
|
95
|
+
- lib/kleisli/validation/version.rb
|
96
|
+
- test/kleisli/validation_test.rb
|
97
|
+
- test/test_helper.rb
|
98
|
+
homepage: ''
|
99
|
+
licenses:
|
100
|
+
- MIT
|
101
|
+
post_install_message:
|
102
|
+
rdoc_options: []
|
103
|
+
require_paths:
|
104
|
+
- lib
|
105
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
106
|
+
none: false
|
107
|
+
requirements:
|
108
|
+
- - ! '>='
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
112
|
+
none: false
|
113
|
+
requirements:
|
114
|
+
- - ! '>='
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: '0'
|
117
|
+
requirements: []
|
118
|
+
rubyforge_project:
|
119
|
+
rubygems_version: 1.8.24
|
120
|
+
signing_key:
|
121
|
+
specification_version: 3
|
122
|
+
summary: Validation Monad for Kleisli gem
|
123
|
+
test_files:
|
124
|
+
- test/kleisli/validation_test.rb
|
125
|
+
- test/test_helper.rb
|
126
|
+
has_rdoc:
|