README.rdoc in rumonade-0.3.0 vs README.rdoc in rumonade-0.4.0
- old
+ new
@@ -1,12 +1,12 @@
{<img src="https://secure.travis-ci.org/ms-ati/rumonade.png?branch=master" alt="Build Status" />}[http://travis-ci.org/ms-ati/rumonade]
= Rumonade[https://rubygems.org/gems/rumonade]
-Project: github[http://github.com/ms-ati/rumonade]
+*_Project_*: github[http://github.com/ms-ati/rumonade]
-Documentation: rubydoc.info[http://rubydoc.info/gems/rumonade/frames]
+*_Documentation_*: rubydoc.info[http://rubydoc.info/gems/rumonade/frames]
== 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]
Are you working in both the Scala[http://www.scala-lang.org] and Ruby[http://www.ruby-lang.org] worlds,
and finding that you miss some of the practical benefits of Scala's
@@ -15,24 +15,24 @@
The goal of this library is to make the most common and useful Scala monadic idioms available in Ruby via the following classes:
* {Rumonade::Option Option}
* {Rumonade::ArrayExtensions Array}
* {Rumonade::Either Either}
-* Hash -- _coming_ _soon!_
+* {Rumonade::Hash Hash}
* (_more_ _TBD_)
Syntactic support for scala-like for-comprehensions[http://www.scala-lang.org/node/111] will be implemented
-as a sequence of calls to #flat_map, #select, etc, modelling Scala's
+as a sequence of calls to #flat_map, #select, etc, modeling Scala's
approach[http://stackoverflow.com/questions/3754089/scala-for-comprehension/3754568#3754568].
Support for an all_catch[http://www.scala-lang.org/archives/downloads/distrib/files/nightly/docs/library/scala/util/control/Exception$.html]
idiom will be implemented to turn blocks which might throw exceptions into Option or Either
results. If this proves useful (and a good fit for Ruby), then more narrow functional catchers can be implemented as well.
-== Usage
+== Usage Examples
-==== {Rumonade::Option Option}: handle _possibly_ _nil_ values in a _functional_ fashion:
+=== {Rumonade::Option Option}: handle _possibly_ _nil_ values in a _functional_ fashion:
def format_date_in_march(time_or_date_or_nil)
Option(time_or_date_or_nil). # wraps possibly-nil value in an Option monad (Some or None)
map(&:to_date). # transforms a contained Time value into a Date value
select {|d| d.month == 3}. # filters out non-matching Date values (Some becomes None)
@@ -47,11 +47,12 @@
Note:
* each step of the chained computations above are functionally isolated
* the value can notionally _start_ as nil, or _become_ nil during a computation, without effecting any other chained computations
-==== {Rumonade::Either Either}: handle failures ({Rumonade::Left Left}) and successes ({Rumonade::Right Right}) in a _functional_ fashion:
+---
+=== {Rumonade::Either Either}: handle failures ({Rumonade::Left Left}) and successes ({Rumonade::Right Right}) in a _functional_ fashion:
def find_person(name)
case name
when /Jack/i, /John/i
Right(name.capitalize)
@@ -66,21 +67,44 @@
# failure looks like this:
find_person("Jill")
# => Left("No such person: Jill")
- # on the 'happy path', we can functionally combine and transform successes:
- (find_person("Jack").lift_to_a + find_person("John").lift_to_a).right.map { |*names| names.join(" and ") }
+ # lift the contained values into Array, in order to combine them:
+ find_person("Joan").lift_to_a
+ # => Left(["No such person: Joan"])
+
+ # on the 'happy path', combine and transform successes into a single success result:
+ (find_person("Jack").lift_to_a +
+ find_person("John").lift_to_a).right.map { |*names| names.join(" and ") }
# => Right("Jack and John")
- # while the failure cases are easily handled as well:
+ # but if there were errors, we still have a Left with all the errors inside:
(find_person("Jack").lift_to_a +
find_person("John").lift_to_a +
find_person("Jill").lift_to_a +
find_person("Joan").lift_to_a).right.map { |*names| names.join(" and ") }
- # => Left("No such person: Jill", "No such person: Joan")
+ # => Left(["No such person: Jill", "No such person: Joan"])
+ # equivalent to the previous example, but shorter:
+ %w(Jack John Jill Joan).map { |nm| find_person(nm).lift_to_a }.inject(:+).
+ right.map { |*names| names.join(" and ") }
+ # => Left(["No such person: Jill", "No such person: Joan"])
+
+---
+=== {Rumonade::Hash Hash}:
+
+ h = { "Foo" => 1, "Bar" => 2, "Baz" => 3 }
+ h = h.flat_map { |k, v| { k => v, k.upcase => v * 10 } }
+ # => {"Foo"=>1, "FOO"=>10, "Bar"=>2, "BAR"=>20, "Baz"=>3, "BAZ"=>30}
+ h = h.select { |k, v| k =~ /^b/i }
+ # => {"Bar"=>2, "BAR"=>20, "Baz"=>3, "BAZ"=>30}
+ h.get("Bar")
+ # => Some(2)
+ h.get("Foo")
+ # => None
+
(_more_ _examples_ _coming_ _soon_...)
== Approach
There have been many[http://moonbase.rydia.net/mental/writings/programming/monads-in-ruby/00introduction.html]
@@ -94,13 +118,16 @@
The priorities for Rumonade are:
1. Practical usability in day-to-day Ruby
* <b>don't</b> mess up normal idioms of the language (e.g., Hash#map)
* <b>don't</b> slow down normal idioms of the language (e.g., Array#map)
2. Rubyish-ness of usage
- * Monad is a mix-in, requiring methods self.unit and #bind be implemented by target classes
- * Prefer blocks to lambda/Procs where possible
+ * Monad is a mix-in, requiring methods +self.unit+ and +#bind+ be implemented by target classes
+ * Prefer blocks to lambda/Procs where possible, but allow both
3. Equivalent idioms to Scala where possible
== Status
-Option, Either, and Array are complete.
-Please try it out, and let me know what you think!
+Option, Either, Array, and Hash are already usable.
+
+<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.
+
+Please try it out, and let me know what you think! Suggestions are always welcome.