rumonade 0.3.0 → 0.4.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/.travis.yml CHANGED
@@ -1,3 +1,6 @@
1
1
  language: ruby
2
2
  rvm:
3
- - 1.9.2
3
+ - 1.9.2
4
+ - 1.9.3
5
+ - jruby-19mode
6
+ - rbx-19mode
data/HISTORY.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # HISTORY
2
2
 
3
+ ## v0.4.0 (Nov 11, 2012)
4
+
5
+ - added scala-like extensions to Hash
6
+ - See full list @ https://github.com/ms-ati/rumonade/compare/v0.3.0...v0.4.0
7
+
3
8
  ## v0.3.0 (Apr 29, 2012)
4
9
 
5
10
  - added Either#lift_to_a (and #lift)
data/MIT-LICENSE.txt CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2011 Marc Siegel
1
+ Copyright (c) 2012 Marc Siegel
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
data/README.rdoc CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  = Rumonade[https://rubygems.org/gems/rumonade]
4
4
 
5
- Project: github[http://github.com/ms-ati/rumonade]
5
+ *_Project_*: github[http://github.com/ms-ati/rumonade]
6
6
 
7
- Documentation: rubydoc.info[http://rubydoc.info/gems/rumonade/frames]
7
+ *_Documentation_*: rubydoc.info[http://rubydoc.info/gems/rumonade/frames]
8
8
 
9
9
  == A Ruby[http://www.ruby-lang.org] Monad[http://en.wikipedia.org/wiki/Monad_(functional_programming)] Library, Inspired by Scala[http://www.scala-lang.org]
10
10
 
@@ -17,20 +17,20 @@ The goal of this library is to make the most common and useful Scala monadic idi
17
17
  * {Rumonade::Option Option}
18
18
  * {Rumonade::ArrayExtensions Array}
19
19
  * {Rumonade::Either Either}
20
- * Hash -- _coming_ _soon!_
20
+ * {Rumonade::Hash Hash}
21
21
  * (_more_ _TBD_)
22
22
 
23
23
  Syntactic support for scala-like for-comprehensions[http://www.scala-lang.org/node/111] will be implemented
24
- as a sequence of calls to #flat_map, #select, etc, modelling Scala's
24
+ as a sequence of calls to #flat_map, #select, etc, modeling Scala's
25
25
  approach[http://stackoverflow.com/questions/3754089/scala-for-comprehension/3754568#3754568].
26
26
 
27
27
  Support for an all_catch[http://www.scala-lang.org/archives/downloads/distrib/files/nightly/docs/library/scala/util/control/Exception$.html]
28
28
  idiom will be implemented to turn blocks which might throw exceptions into Option or Either
29
29
  results. If this proves useful (and a good fit for Ruby), then more narrow functional catchers can be implemented as well.
30
30
 
31
- == Usage
31
+ == Usage Examples
32
32
 
33
- ==== {Rumonade::Option Option}: handle _possibly_ _nil_ values in a _functional_ fashion:
33
+ === {Rumonade::Option Option}: handle _possibly_ _nil_ values in a _functional_ fashion:
34
34
 
35
35
  def format_date_in_march(time_or_date_or_nil)
36
36
  Option(time_or_date_or_nil). # wraps possibly-nil value in an Option monad (Some or None)
@@ -49,7 +49,8 @@ Note:
49
49
  * each step of the chained computations above are functionally isolated
50
50
  * the value can notionally _start_ as nil, or _become_ nil during a computation, without effecting any other chained computations
51
51
 
52
- ==== {Rumonade::Either Either}: handle failures ({Rumonade::Left Left}) and successes ({Rumonade::Right Right}) in a _functional_ fashion:
52
+ ---
53
+ === {Rumonade::Either Either}: handle failures ({Rumonade::Left Left}) and successes ({Rumonade::Right Right}) in a _functional_ fashion:
53
54
 
54
55
  def find_person(name)
55
56
  case name
@@ -68,16 +69,39 @@ Note:
68
69
  find_person("Jill")
69
70
  # => Left("No such person: Jill")
70
71
 
71
- # on the 'happy path', we can functionally combine and transform successes:
72
- (find_person("Jack").lift_to_a + find_person("John").lift_to_a).right.map { |*names| names.join(" and ") }
72
+ # lift the contained values into Array, in order to combine them:
73
+ find_person("Joan").lift_to_a
74
+ # => Left(["No such person: Joan"])
75
+
76
+ # on the 'happy path', combine and transform successes into a single success result:
77
+ (find_person("Jack").lift_to_a +
78
+ find_person("John").lift_to_a).right.map { |*names| names.join(" and ") }
73
79
  # => Right("Jack and John")
74
80
 
75
- # while the failure cases are easily handled as well:
81
+ # but if there were errors, we still have a Left with all the errors inside:
76
82
  (find_person("Jack").lift_to_a +
77
83
  find_person("John").lift_to_a +
78
84
  find_person("Jill").lift_to_a +
79
85
  find_person("Joan").lift_to_a).right.map { |*names| names.join(" and ") }
80
- # => Left("No such person: Jill", "No such person: Joan")
86
+ # => Left(["No such person: Jill", "No such person: Joan"])
87
+
88
+ # equivalent to the previous example, but shorter:
89
+ %w(Jack John Jill Joan).map { |nm| find_person(nm).lift_to_a }.inject(:+).
90
+ right.map { |*names| names.join(" and ") }
91
+ # => Left(["No such person: Jill", "No such person: Joan"])
92
+
93
+ ---
94
+ === {Rumonade::Hash Hash}:
95
+
96
+ h = { "Foo" => 1, "Bar" => 2, "Baz" => 3 }
97
+ h = h.flat_map { |k, v| { k => v, k.upcase => v * 10 } }
98
+ # => {"Foo"=>1, "FOO"=>10, "Bar"=>2, "BAR"=>20, "Baz"=>3, "BAZ"=>30}
99
+ h = h.select { |k, v| k =~ /^b/i }
100
+ # => {"Bar"=>2, "BAR"=>20, "Baz"=>3, "BAZ"=>30}
101
+ h.get("Bar")
102
+ # => Some(2)
103
+ h.get("Foo")
104
+ # => None
81
105
 
82
106
  (_more_ _examples_ _coming_ _soon_...)
83
107
 
@@ -96,11 +120,14 @@ The priorities for Rumonade are:
96
120
  * <b>don't</b> mess up normal idioms of the language (e.g., Hash#map)
97
121
  * <b>don't</b> slow down normal idioms of the language (e.g., Array#map)
98
122
  2. Rubyish-ness of usage
99
- * Monad is a mix-in, requiring methods self.unit and #bind be implemented by target classes
100
- * Prefer blocks to lambda/Procs where possible
123
+ * Monad is a mix-in, requiring methods +self.unit+ and +#bind+ be implemented by target classes
124
+ * Prefer blocks to lambda/Procs where possible, but allow both
101
125
  3. Equivalent idioms to Scala where possible
102
126
 
103
127
  == Status
104
128
 
105
- Option, Either, and Array are complete.
106
- Please try it out, and let me know what you think!
129
+ Option, Either, Array, and Hash are already usable.
130
+
131
+ <b><em>Supported Ruby versions</em></b>: MRI 1.9.2, MRI 1.9.3, JRuby in 1.9 mode, and Rubinius in 1.9 mode.
132
+
133
+ Please try it out, and let me know what you think! Suggestions are always welcome.
data/lib/rumonade.rb CHANGED
@@ -26,6 +26,7 @@ require "rumonade/monad"
26
26
  require "rumonade/lazy_identity"
27
27
  require "rumonade/option"
28
28
  require "rumonade/array"
29
+ require "rumonade/hash"
29
30
  require "rumonade/either"
30
31
  require "rumonade/error_handling"
31
32
 
@@ -14,6 +14,9 @@ module Rumonade
14
14
  end
15
15
 
16
16
  module InstanceMethods
17
+ # Preserve native +map+ method for performance
18
+ METHODS_TO_REPLACE_WITH_MONAD = Monad::DEFAULT_METHODS_TO_REPLACE_WITH_MONAD - [:map]
19
+
17
20
  def bind(lam = nil, &blk)
18
21
  inject(self.class.empty) { |arr, elt| arr + (lam || blk).call(elt).to_a }
19
22
  end
@@ -5,6 +5,10 @@ module Rumonade
5
5
  # The data constructors {Rumonade::Left} and {Rumonade::Right} represent the two possible values.
6
6
  # The +Either+ type is often used as an alternative to {Rumonade::Option} where {Rumonade::Left} represents
7
7
  # failure (by convention) and {Rumonade::Right} is akin to {Rumonade::Some}.
8
+ #
9
+ # This implementation of +Either+ also contains ideas from the +Validation+ class in the
10
+ # +scalaz+ library.
11
+ #
8
12
  # @abstract
9
13
  class Either
10
14
  def initialize
@@ -0,0 +1,35 @@
1
+ require 'rumonade/monad'
2
+
3
+ module Rumonade
4
+ # TODO: Document use of Hash as a Monad
5
+ module HashExtensions
6
+ module ClassMethods
7
+ def unit(value)
8
+ raise ArgumentError, "argument not a 2-element Array for Hash.unit" unless (value.is_a?(Array) && value.size == 2)
9
+ Hash[*value]
10
+ end
11
+
12
+ def empty
13
+ {}
14
+ end
15
+ end
16
+
17
+ module InstanceMethods
18
+ # Preserve native +map+ and +flatten+ methods for compatibility
19
+ METHODS_TO_REPLACE_WITH_MONAD = Monad::DEFAULT_METHODS_TO_REPLACE_WITH_MONAD - [:map, :flatten]
20
+
21
+ def bind(lam = nil, &blk)
22
+ inject(self.class.empty) { |hsh, elt| hsh.merge((lam || blk).call(elt)) }
23
+ end
24
+
25
+ # @return [Option] a Some containing the value associated with +key+, or None if not present
26
+ def get(key)
27
+ Option(self[key])
28
+ end
29
+ end
30
+ end
31
+ end
32
+
33
+ Hash.send(:extend, Rumonade::HashExtensions::ClassMethods)
34
+ Hash.send(:include, Rumonade::HashExtensions::InstanceMethods)
35
+ Hash.send(:include, Rumonade::Monad)
@@ -1,19 +1,25 @@
1
1
  module Rumonade
2
- # Mix-in for common monad functionality dependent on implementation of monadic methods unit and bind
2
+ # Mix-in for common monad functionality dependent on implementation of monadic methods +unit+ and +bind+
3
3
  #
4
4
  # Notes:
5
- # * classes should include this module AFTER defining the monadic methods unit and bind
5
+ # * Classes should include this module AFTER defining the monadic methods +unit+ and +bind+
6
+ # * When +Monad+ is mixed into a class, if the class already contains methods in
7
+ # {METHODS_TO_REPLACE}, they will be renamed to add the suffix +_without_monad+,
8
+ # and replaced with the method defined here which has the suffix +_with_monad+
6
9
  #
7
10
  module Monad
8
- METHODS_TO_REPLACE = [:flat_map, :flatten] # :nodoc:
11
+ # Methods to replace when mixed in -- unless class defines +METHODS_TO_REPLACE_WITH_MONAD+
12
+ DEFAULT_METHODS_TO_REPLACE_WITH_MONAD = [:map, :flat_map, :flatten]
13
+
14
+ # When mixed into a class, this callback is executed
15
+ def self.included(base)
16
+ methods_to_replace = base::METHODS_TO_REPLACE_WITH_MONAD rescue DEFAULT_METHODS_TO_REPLACE_WITH_MONAD
9
17
 
10
- def self.included(base) # :nodoc:
11
18
  base.class_eval do
12
19
  # optimization: replace flat_map with an alias for bind, as they are identical
13
20
  alias_method :flat_map_with_monad, :bind
14
21
 
15
- # force only a few methods to be aliased to monad versions; others can stay with native or Enumerable versions
16
- METHODS_TO_REPLACE.each do |method_name|
22
+ methods_to_replace.each do |method_name|
17
23
  alias_method "#{method_name}_without_monad".to_sym, method_name if public_instance_methods.include? method_name
18
24
  alias_method method_name, "#{method_name}_with_monad".to_sym
19
25
  end
@@ -28,19 +34,26 @@ module Rumonade
28
34
  end
29
35
 
30
36
  # Returns a monad whose elements are the results of applying the given function to each element in this monad
31
- def map(lam = nil, &blk)
37
+ #
38
+ # NOTE: normally aliased as +map+ when +Monad+ is mixed into a class
39
+ def map_with_monad(lam = nil, &blk)
32
40
  bind { |v| self.class.unit((lam || blk).call(v)) }
33
41
  end
34
42
 
35
43
  # Returns the results of applying the given function to each element in this monad
44
+ #
45
+ # NOTE: normally aliased as +flat_map+ when +Monad+ is mixed into a class
36
46
  def flat_map_with_monad(lam = nil, &blk)
37
47
  bind(lam || blk)
38
48
  end
39
49
 
40
50
  # Returns a monad whose elements are the ultimate (non-monadic) values contained in all nested monads
41
51
  #
42
- # [1] == [[Some(Some(1)), Some(Some(None))], [None]].flatten
43
- # => true
52
+ # NOTE: normally aliased as +flatten+ when +Monad+ is mixed into a class
53
+ #
54
+ # @example
55
+ # [Some(Some(1)), Some(Some(None))], [None]].flatten
56
+ # #=> [1]
44
57
  #
45
58
  def flatten_with_monad
46
59
  bind { |x| x.is_a?(Monad) ? x.flatten_with_monad : self.class.unit(x) }
@@ -57,6 +70,16 @@ module Rumonade
57
70
  # This method is equivalent to the Scala flatten call (single-level flattening), whereas #flatten is in keeping
58
71
  # with the native Ruby flatten calls (multiple-level flattening).
59
72
  #
73
+ # @example
74
+ # [Some(Some(1)), Some(Some(None))], [None]].shallow_flatten
75
+ # #=> [Some(Some(1)), Some(Some(None)), None]
76
+ # [Some(Some(1)), Some(Some(None)), None].shallow_flatten
77
+ # #=> [Some(1), Some(None)]
78
+ # [Some(1), Some(None)].shallow_flatten
79
+ # #=> [1, None]
80
+ # [1, None].shallow_flatten
81
+ # #=> [1]
82
+ #
60
83
  def shallow_flatten
61
84
  bind { |x| x.is_a?(Monad) ? x : self.class.unit(x) }
62
85
  end
@@ -1,3 +1,3 @@
1
1
  module Rumonade
2
- VERSION = "0.3.0"
2
+ VERSION = "0.4.0"
3
3
  end
data/test/hash_test.rb ADDED
@@ -0,0 +1,61 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/test_helper')
2
+
3
+ class HashTest < Test::Unit::TestCase
4
+ include Rumonade
5
+ include MonadAxiomTestHelpers
6
+
7
+ def test_when_unit_with_2_elt_array_returns_hash
8
+ assert_equal({ :k => :v }, Hash.unit([:k, :v]))
9
+ end
10
+
11
+ def test_when_unit_with_non_2_elt_array_raises
12
+ assert_raise(ArgumentError) { Hash.unit([1]) }
13
+ assert_raise(ArgumentError) { Hash.unit([1, 2, 3]) }
14
+ assert_raise(ArgumentError) { Hash.unit([1, 2, 3, 4]) }
15
+ assert_raise(ArgumentError) { Hash.unit("not an array at all") }
16
+ end
17
+
18
+ def test_when_empty_returns_empty_array
19
+ assert_equal({}, Hash.empty)
20
+ end
21
+
22
+ def test_monad_axioms
23
+ f = lambda { |p| Hash.unit([p[0].downcase, p[1] * 2]) }
24
+ g = lambda { |p| Hash.unit([p[0].upcase, p[1] * 5]) }
25
+ [["Foo", 1], ["Bar", 2]].each do |value|
26
+ assert_monad_axiom_1(Hash, value, f)
27
+ assert_monad_axiom_2(Hash.unit(value))
28
+ assert_monad_axiom_3(Hash.unit(value), f, g)
29
+ end
30
+ end
31
+
32
+ def test_flat_map_behaves_correctly
33
+ assert_equal({ "FOO" => 2, "BAR" => 4 }, { "Foo" => 1, "Bar" => 2 }.flat_map { |p| { p[0].upcase => p[1] * 2 } })
34
+ end
35
+
36
+ # Special case: because Hash#map is built into Ruby, must preserve existing behavior
37
+ def test_map_still_behaves_normally_returning_array_of_arrays
38
+ assert_equal([["FOO", 2], ["BAR", 4]], { "Foo" => 1, "Bar" => 2 }.map { |p| [p[0].upcase, p[1] * 2] })
39
+ end
40
+
41
+ # We add Hash#map_with_monad to return a Hash, as one might expect Hash#map to do
42
+ def test_map_with_monad_behaves_correctly_returning_hash
43
+ assert_equal({ "FOO" => 2, "BAR" => 4 }, { "Foo" => 1, "Bar" => 2 }.map_with_monad { |p| [p[0].upcase, p[1] * 2] })
44
+ end
45
+
46
+ # Special case: because Hash#flatten is built into Ruby, must preserve existing behavior
47
+ unless (RUBY_ENGINE.to_s rescue nil) == 'rbx' # Except on Rubinius, where it's broken, apparently
48
+ def test_flatten_still_behaves_normaly_returning_array_of_alternating_keys_and_values
49
+ assert_equal ["Foo", 1, "Bar", 2], { "Foo" => 1, "Bar" => 2 }.flatten
50
+ end
51
+ end
52
+
53
+ def test_shallow_flatten_raises_type_error
54
+ assert_raise(TypeError) { { "Foo" => "Bar" }.shallow_flatten }
55
+ end
56
+
57
+ def test_get_returns_option_of_value
58
+ assert_equal None, { "Foo" => 1, "Bar" => 2 }.get("Baz")
59
+ assert_equal Some(1), { "Foo" => 1, "Bar" => 2 }.get("Foo")
60
+ end
61
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rumonade
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
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-04-29 00:00:00.000000000 Z
12
+ date: 2012-11-12 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: test-unit
@@ -63,6 +63,7 @@ files:
63
63
  - lib/rumonade/either.rb
64
64
  - lib/rumonade/error_handling.rb
65
65
  - lib/rumonade/errors.rb
66
+ - lib/rumonade/hash.rb
66
67
  - lib/rumonade/lazy_identity.rb
67
68
  - lib/rumonade/monad.rb
68
69
  - lib/rumonade/option.rb
@@ -71,6 +72,7 @@ files:
71
72
  - test/array_test.rb
72
73
  - test/either_test.rb
73
74
  - test/error_handling_test.rb
75
+ - test/hash_test.rb
74
76
  - test/lazy_identity_test.rb
75
77
  - test/option_test.rb
76
78
  - test/test_helper.rb
@@ -94,7 +96,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
94
96
  version: '0'
95
97
  requirements: []
96
98
  rubyforge_project: rumonade
97
- rubygems_version: 1.8.21
99
+ rubygems_version: 1.8.24
98
100
  signing_key:
99
101
  specification_version: 3
100
102
  summary: A Scala-inspired Monad library for Ruby
@@ -102,7 +104,7 @@ test_files:
102
104
  - test/array_test.rb
103
105
  - test/either_test.rb
104
106
  - test/error_handling_test.rb
107
+ - test/hash_test.rb
105
108
  - test/lazy_identity_test.rb
106
109
  - test/option_test.rb
107
110
  - test/test_helper.rb
108
- has_rdoc: