rumonade 0.1.0

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 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