rumonade 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,8 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ .idea
6
+ html
7
+ rdoc
8
+
data/CHANGELOG.rdoc ADDED
@@ -0,0 +1,7 @@
1
+ === 0.1.0 / 2011-09-17
2
+
3
+ * Initial Feature Set
4
+
5
+ * general implementation and testing of monadic laws based on `unit` and `bind`
6
+ * scala-like Option class w/ Some & None
7
+ * scala-like extensions to Array
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in rumonade.gemspec
4
+ gemspec
data/MIT-LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Marc Siegel
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,15 @@
1
+ = Rumonade
2
+ === A Ruby Monad Library, Inspired by Scala
3
+
4
+ Are you working in both the Scala and Ruby worlds, and finding that you miss some of the practical benefits
5
+ of Scala's monads in Ruby? Then Rumonade is for you.
6
+
7
+ The goal of this library is to make the most common scala monad idioms available in ruby:
8
+ * Option
9
+ * Arrays of Options / Options of Arrays
10
+ * Either
11
+ * for comprehensions
12
+
13
+ This code is in a very early state, but the Option monad is already present.
14
+ Please try it out and let me know what you think.
15
+
data/Rakefile ADDED
@@ -0,0 +1,25 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |test|
5
+ test.libs << 'lib' << 'test'
6
+ test.pattern = 'test/**/*_test.rb'
7
+ end
8
+
9
+ #require "rdoc/task"
10
+ #RDoc::Task.new do |rdoc|
11
+ # rdoc.main = "README.rdoc"
12
+ # rdoc.rdoc_files.include("README.rdoc", "CHANGELOG.rdoc", "lib/**/*.rb")
13
+ #end
14
+
15
+ require 'rake/rdoctask'
16
+ Rake::RDocTask.new do |rdoc|
17
+ require File.expand_path("../lib/rumonade/version", __FILE__)
18
+ rdoc.rdoc_dir = 'rdoc'
19
+ rdoc.options << '--line-numbers' << '--inline-source'
20
+ rdoc.title = "Rumonade #{Rumonade::VERSION}"
21
+ rdoc.main = 'README.rdoc'
22
+ rdoc.rdoc_files.include('README.rdoc')
23
+ rdoc.rdoc_files.include('CHANGELOG.rdoc')
24
+ rdoc.rdoc_files.include('lib/**/*.rb')
25
+ end
data/lib/rumonade.rb ADDED
@@ -0,0 +1,41 @@
1
+ #--
2
+ # Copyright (c) 2011 Marc Siegel
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ #++
23
+ require "rumonade/version"
24
+ require "rumonade/monad"
25
+ require "rumonade/lazy_identity"
26
+ require "rumonade/option"
27
+ require "rumonade/array"
28
+
29
+ # = Rumonade
30
+ #
31
+ # Rumonade is a ruby module providing a scala-like system of monads, including:
32
+ # * Option
33
+ # * Array
34
+ #
35
+ # See the examples in link:files/README_rdoc.html for more insight.
36
+ #
37
+ module Rumonade
38
+ end
39
+
40
+ # bring into global namespace Monad, Option, etc
41
+ include Rumonade
@@ -0,0 +1,26 @@
1
+ require 'rumonade/monad'
2
+
3
+ module Rumonade
4
+ module ArrayExtensions
5
+ module ClassMethods
6
+ def unit(value)
7
+ [value]
8
+ end
9
+
10
+ def empty
11
+ []
12
+ end
13
+ end
14
+
15
+ module InstanceMethods
16
+ def bind(lam = nil, &blk)
17
+ f = lam || blk
18
+ inject([]) { |arr, elt| arr + f.call(elt).to_a }
19
+ end
20
+ end
21
+ end
22
+ end
23
+
24
+ Array.send(:extend, Rumonade::ArrayExtensions::ClassMethods)
25
+ Array.send(:include, Rumonade::ArrayExtensions::InstanceMethods)
26
+ Array.send(:include, Rumonade::Monad)
@@ -0,0 +1,27 @@
1
+ # Adapted from http://stackoverflow.com/questions/2709361/monad-equivalent-in-ruby
2
+ class LazyIdentity
3
+ def initialize(lam = nil, &blk)
4
+ @lazy = lam || blk
5
+ @lazy.is_a?(Proc) || raise(ArgumentError, "not a Proc")
6
+ @lazy.arity.zero? || raise(ArgumentError, "arity must be 0, was #{@lazy.arity}")
7
+ end
8
+
9
+ attr_reader :lazy
10
+
11
+ def force
12
+ @lazy[]
13
+ end
14
+
15
+ def self.unit(lam = nil, &blk)
16
+ LazyIdentity.new(lam || blk)
17
+ end
18
+
19
+ def bind(lam = nil, &blk)
20
+ f = lam || blk
21
+ f[@lazy]
22
+ end
23
+
24
+ def ==(other)
25
+ other.is_a?(LazyIdentity) && other.force == force
26
+ end
27
+ end
@@ -0,0 +1,36 @@
1
+ module Rumonade
2
+ # TODO: Document this
3
+ module Monad
4
+ METHODS_TO_REPLACE = [:flat_map, :flatten]
5
+
6
+ def self.included(base)
7
+ base.class_eval do
8
+ alias_method :flat_map_with_monad, :bind
9
+
10
+ # force only a few methods to be aliased to monad versions; others can stay with native or Enumerable versions
11
+ METHODS_TO_REPLACE.each do |method_name|
12
+ alias_method "#{method_name}_without_monad".to_sym, method_name if public_instance_methods.include? method_name
13
+ alias_method method_name, "#{method_name}_with_monad".to_sym
14
+ end
15
+ end
16
+ end
17
+
18
+ include Enumerable
19
+
20
+ def map(lam = nil, &blk)
21
+ bind { |v| (lam || blk).call(v) }
22
+ end
23
+
24
+ def each(lam = nil, &blk)
25
+ map(lam || blk); nil
26
+ end
27
+
28
+ def shallow_flatten
29
+ bind { |x| x.is_a?(Monad) ? x : self.class.unit(x) }
30
+ end
31
+
32
+ def flatten_with_monad
33
+ bind { |x| x.is_a?(Monad) ? x.flatten_with_monad : self.class.unit(x) }
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,102 @@
1
+ require 'singleton'
2
+ require 'rumonade/monad'
3
+
4
+ module Rumonade # :nodoc:
5
+ # TODO: Document this
6
+ class Option
7
+ class << self
8
+ def unit(value)
9
+ Rumonade.Option(value)
10
+ end
11
+
12
+ def empty
13
+ None
14
+ end
15
+ end
16
+
17
+ def initialize
18
+ raise(TypeError, "class Option is abstract; cannot be instantiated") if self.class == Option
19
+ end
20
+
21
+ def bind(lam = nil, &blk)
22
+ f = lam || blk
23
+ empty? ? self : f.call(value)
24
+ end
25
+
26
+ include Monad
27
+
28
+ def empty?
29
+ raise(NotImplementedError)
30
+ end
31
+
32
+ def get
33
+ if !empty? then value else raise NoSuchElementError end
34
+ end
35
+
36
+ def get_or_else(val_or_lam = nil, &blk)
37
+ v_or_f = val_or_lam || blk
38
+ if !empty? then value else (v_or_f.respond_to?(:call) ? v_or_f.call : v_or_f) end
39
+ end
40
+
41
+ def or_nil
42
+ get_or_else(nil)
43
+ end
44
+ end
45
+
46
+ # TODO: Document this
47
+ class Some < Option
48
+ def initialize(value)
49
+ @value = value
50
+ end
51
+
52
+ attr_reader :value
53
+
54
+ def empty?
55
+ false
56
+ end
57
+
58
+ def ==(other)
59
+ other.is_a?(Some) && other.value == value
60
+ end
61
+
62
+ def to_s
63
+ "Some(#{value.to_s})"
64
+ end
65
+ end
66
+
67
+ # TODO: Document this
68
+ class NoneClass < Option
69
+ include Singleton
70
+
71
+ def empty?
72
+ true
73
+ end
74
+
75
+ def ==(other)
76
+ other.equal?(self.class.instance)
77
+ end
78
+
79
+ def to_s
80
+ "None"
81
+ end
82
+ end
83
+
84
+ # TODO: Document this
85
+ class NoSuchElementError < RuntimeError; end
86
+
87
+ # TODO: Document this
88
+ def Option(value)
89
+ value.nil? ? None : Some(value)
90
+ end
91
+
92
+ # TODO: Document this
93
+ def Some(value)
94
+ Some.new(value)
95
+ end
96
+
97
+ # TODO: Document this
98
+ None = NoneClass.instance
99
+
100
+ module_function :Option, :Some
101
+ public :Option, :Some
102
+ end
@@ -0,0 +1,3 @@
1
+ module Rumonade
2
+ VERSION = "0.1.0"
3
+ end
data/rumonade.gemspec ADDED
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "rumonade/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "rumonade"
7
+ s.version = Rumonade::VERSION
8
+ s.authors = ["Marc Siegel"]
9
+ s.email = ["marc@usainnov.com"]
10
+ s.homepage = "http://github.com/ms-ati/rumonade"
11
+ s.summary = "A Scala-inspired Monad library for Ruby"
12
+ s.description = "A Scala-inspired Monad library for Ruby, aiming to share the most common idioms for folks working in both languages. Includes Option, Array, etc."
13
+
14
+ s.rubyforge_project = "rumonade"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+
21
+ # specify any dependencies here; for example:
22
+ #s.add_development_dependency "rdoc"
23
+ #s.add_development_dependency "test-unit"
24
+ #s.add_development_dependency "rr"
25
+ end
@@ -0,0 +1,45 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/test_helper')
2
+
3
+ class ArrayTest < Test::Unit::TestCase
4
+ include Rumonade
5
+ include MonadAxiomTestHelpers
6
+
7
+ def test_when_unit_returns_1_elt_array
8
+ assert_equal [1], Array.unit(1)
9
+ end
10
+
11
+ def test_when_empty_returns_empty_array
12
+ assert_equal [], Array.empty
13
+ end
14
+
15
+ def test_monad_axioms
16
+ f = lambda { |x| Array.unit(x && x * 2) }
17
+ g = lambda { |x| Array.unit(x && x * 5) }
18
+ [1, 42].each do |value|
19
+ assert_monad_axiom_1(Array, value, f)
20
+ assert_monad_axiom_2(Array.unit(value))
21
+ assert_monad_axiom_3(Array.unit(value), f, g)
22
+ end
23
+ end
24
+
25
+ def test_flat_map_behaves_correctly
26
+ assert_equal ["FOO", "BAR"], ["foo", "bar"].flat_map { |s| [s.upcase] }
27
+ end
28
+
29
+ def test_map_behaves_correctly
30
+ assert_equal ["FOO", "BAR"], ["foo", "bar"].map { |s| s.upcase }
31
+ end
32
+
33
+ def test_shallow_flatten_behaves_correctly
34
+ assert_equal [0, 1, [2], [[3]], [[[4]]]], [0, [1], [[2]], [[[3]]], [[[[4]]]]].shallow_flatten
35
+ assert_equal [1], [None, Some(1)].shallow_flatten
36
+ assert_equal [1, Some(2)], [None, Some(1), Some(Some(2))].shallow_flatten
37
+ assert_equal [Some(Some(None))], [Some(Some(Some(None)))].shallow_flatten
38
+ end
39
+
40
+ def test_flatten_behaves_correctly
41
+ assert_equal [0, 1, 2, 3, 4], [0, [1], [[2]], [[[3]]], [[[[4]]]]].flatten
42
+ assert_equal [1, 2], [None, Some(1), Some(Some(2))].flatten
43
+ assert_equal [], [Some(Some(Some(None)))].flatten
44
+ end
45
+ end
@@ -0,0 +1,16 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), 'test_helper'))
2
+
3
+ class LazyIdentityTest < Test::Unit::TestCase
4
+ include Rumonade
5
+ include MonadAxiomTestHelpers
6
+
7
+ def test_monad_axioms
8
+ f = lambda { |lazy| v = lazy.call * 2; LazyIdentity.new { v } }
9
+ g = lambda { |lazy| v = lazy.call + 5; LazyIdentity.new { v } }
10
+ lazy_value = lambda { 42 } # returns 42 when called
11
+ assert_monad_axiom_1(LazyIdentity, lazy_value, f)
12
+ assert_monad_axiom_2(LazyIdentity.new(lazy_value))
13
+ assert_monad_axiom_3(LazyIdentity.new(lazy_value), f, g)
14
+ end
15
+
16
+ end
@@ -0,0 +1,105 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/test_helper')
2
+
3
+ class OptionTest < Test::Unit::TestCase
4
+ include Rumonade
5
+ include MonadAxiomTestHelpers
6
+
7
+ def test_when_option_with_nil_returns_none_singleton
8
+ assert_same None, Option.unit(nil)
9
+ assert_same None, Option(nil)
10
+ assert_same NoneClass.instance, None
11
+ end
12
+
13
+ def test_when_option_with_value_returns_some
14
+ assert_equal Some(42), Option.unit(42)
15
+ assert_equal Some(42), Option(42)
16
+ assert_equal Some(42), Some.new(42)
17
+ assert_not_equal None, Some(nil)
18
+ end
19
+
20
+ def test_when_option_constructor_raises
21
+ assert_raise(TypeError) { Option.new }
22
+ end
23
+
24
+ def test_monad_axioms
25
+ f = lambda { |x| Option(x && x * 2) }
26
+ g = lambda { |x| Option(x && x * 5) }
27
+ [nil, 42].each do |value|
28
+ assert_monad_axiom_1(Option, value, f)
29
+ assert_monad_axiom_2(Option(value))
30
+ assert_monad_axiom_3(Option(value), f, g)
31
+ end
32
+ end
33
+
34
+ def test_when_empty_returns_none
35
+ assert_equal None, Option.empty
36
+ end
37
+
38
+ def test_when_value_on_some_returns_value_but_on_none_raises
39
+ assert_equal "foo", Some("foo").value
40
+ assert_raise(NoMethodError) { None.value }
41
+ end
42
+
43
+ def test_when_get_on_some_returns_value_but_on_none_raises
44
+ assert_equal "foo", Some("foo").get
45
+ assert_raise(NoSuchElementError) { None.get }
46
+ end
47
+
48
+ def test_when_get_or_else_on_some_returns_value_but_on_none_returns_value_or_executes_block_or_lambda
49
+ assert_equal "foo", Some("foo").get_or_else("bar")
50
+ assert_equal "bar", None.get_or_else("bar")
51
+ assert_equal "blk", None.get_or_else { "blk" }
52
+ assert_equal "lam", None.get_or_else(lambda { "lam"} )
53
+ end
54
+
55
+ def test_when_or_nil_on_some_returns_value_but_on_none_returns_nil
56
+ assert_equal 123, Some(123).or_nil
57
+ assert_nil None.or_nil
58
+ end
59
+
60
+ def test_flat_map_behaves_correctly
61
+ assert_equal Some("FOO"), Some("foo").flat_map { |s| Some(s.upcase) }
62
+ assert_equal None, None.flat_map { |s| Some(s.upcase) }
63
+ end
64
+
65
+ def test_map_behaves_correctly
66
+ assert_equal "FOO", Some("foo").map { |s| s.upcase }
67
+ assert_equal None, None.map { |s| s.upcase }
68
+ end
69
+
70
+ def test_shallow_flatten_behaves_correctly
71
+ assert_equal Some(Some(1)), Some(Some(Some(1))).shallow_flatten
72
+ assert_equal None, Some(None).shallow_flatten
73
+ assert_equal Some(1), Some(1).shallow_flatten
74
+ assert_equal [None, Some(1)], Some([None, Some(1)]).shallow_flatten
75
+ end
76
+
77
+ def test_flatten_behaves_correctly
78
+ assert_equal Some(1), Some(Some(Some(1))).flatten
79
+ assert_equal None, Some(None).flatten
80
+ assert_equal Some(1), Some(1).flatten
81
+ assert_equal [1], Some([None, Some(1)]).flatten
82
+ end
83
+
84
+ def test_to_s_behaves_correctly
85
+ assert_equal "Some(1)", Some(1).to_s
86
+ assert_equal "None", None.to_s
87
+ assert_equal "Some(Some(None))", Some(Some(None)).to_s
88
+ end
89
+
90
+ def test_each_behaves_correctly
91
+ vals = [None, Some(42)].inject([]) { |arr, opt| assert_nil(opt.each { |val| arr << val }); arr }
92
+ assert_equal [42], vals
93
+ end
94
+
95
+ def test_enumerable_methods_are_available
96
+ assert Some(1).all? { |v| v < 10 }
97
+ assert !Some(1).all? { |v| v > 10 }
98
+ assert None.all? { |v| v > 10 }
99
+ end
100
+
101
+ def test_to_a_behaves_correctly
102
+ assert_equal [1], Some(1).to_a
103
+ assert_equal [], None.to_a
104
+ end
105
+ end
@@ -0,0 +1,20 @@
1
+ $:.unshift(File.dirname(File.dirname(File.expand_path(__FILE__))) + '/lib')
2
+ require "rubygems"
3
+ require "test/unit"
4
+ require "rr"
5
+ require "rumonade"
6
+
7
+ # see http://stackoverflow.com/questions/2709361/monad-equivalent-in-ruby
8
+ module MonadAxiomTestHelpers
9
+ def assert_monad_axiom_1(monad_class, value, f)
10
+ assert_equal f[value], monad_class.unit(value).bind(f)
11
+ end
12
+
13
+ def assert_monad_axiom_2(monad)
14
+ assert_equal monad, monad.bind(lambda { |v| monad.class.unit(v) })
15
+ end
16
+
17
+ def assert_monad_axiom_3(monad, f, g)
18
+ assert_equal monad.bind(f).bind(g), monad.bind { |x| f[x].bind(g) }
19
+ end
20
+ end
metadata ADDED
@@ -0,0 +1,74 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rumonade
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.1.0
6
+ platform: ruby
7
+ authors:
8
+ - Marc Siegel
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2011-09-17 00:00:00 Z
14
+ dependencies: []
15
+
16
+ description: A Scala-inspired Monad library for Ruby, aiming to share the most common idioms for folks working in both languages. Includes Option, Array, etc.
17
+ email:
18
+ - marc@usainnov.com
19
+ executables: []
20
+
21
+ extensions: []
22
+
23
+ extra_rdoc_files: []
24
+
25
+ files:
26
+ - .gitignore
27
+ - CHANGELOG.rdoc
28
+ - Gemfile
29
+ - MIT-LICENSE.txt
30
+ - README.rdoc
31
+ - Rakefile
32
+ - lib/rumonade.rb
33
+ - lib/rumonade/array.rb
34
+ - lib/rumonade/lazy_identity.rb
35
+ - lib/rumonade/monad.rb
36
+ - lib/rumonade/option.rb
37
+ - lib/rumonade/version.rb
38
+ - rumonade.gemspec
39
+ - test/array_test.rb
40
+ - test/lazy_identity_test.rb
41
+ - test/option_test.rb
42
+ - test/test_helper.rb
43
+ homepage: http://github.com/ms-ati/rumonade
44
+ licenses: []
45
+
46
+ post_install_message:
47
+ rdoc_options: []
48
+
49
+ require_paths:
50
+ - lib
51
+ required_ruby_version: !ruby/object:Gem::Requirement
52
+ none: false
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: "0"
57
+ required_rubygems_version: !ruby/object:Gem::Requirement
58
+ none: false
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: "0"
63
+ requirements: []
64
+
65
+ rubyforge_project: rumonade
66
+ rubygems_version: 1.8.8
67
+ signing_key:
68
+ specification_version: 3
69
+ summary: A Scala-inspired Monad library for Ruby
70
+ test_files:
71
+ - test/array_test.rb
72
+ - test/lazy_identity_test.rb
73
+ - test/option_test.rb
74
+ - test/test_helper.rb