rumonade 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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: