rumonade 0.1.0 → 0.1.1

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/CHANGELOG.rdoc CHANGED
@@ -1,7 +1,13 @@
1
+ === 0.1.1 / 2011-09-19
2
+
3
+ * Maintenance
4
+ * added a first stab at documentation for Option
5
+ * fixed certain errors with #map
6
+ * added #select
7
+
1
8
  === 0.1.0 / 2011-09-17
2
9
 
3
10
  * Initial Feature Set
4
-
5
11
  * general implementation and testing of monadic laws based on `unit` and `bind`
6
12
  * scala-like Option class w/ Some & None
7
13
  * scala-like extensions to Array
data/README.rdoc CHANGED
@@ -1,15 +1,67 @@
1
- = Rumonade
2
- === A Ruby Monad Library, Inspired by Scala
1
+ = Rumonade[https://rubygems.org/gems/rumonade]
3
2
 
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.
3
+ == 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]
6
4
 
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
5
+ Are you working in both the Scala[http://www.scala-lang.org] and Ruby[http://www.ruby-lang.org] worlds,
6
+ and finding that you miss some of the practical benefits of Scala's
7
+ monads[http://james-iry.blogspot.com/2007/09/monads-are-elephants-part-1.html] in Ruby?
8
+ Then Rumonade is for you.
9
+
10
+ The goal of this library is to make the most common and useful Scala monadic idioms available in Ruby via the following classes:
11
+ * Rumonade::Option
12
+ * Array
10
13
  * Either
11
- * for comprehensions
14
+ * Hash
15
+ * (more TBD)
12
16
 
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.
17
+ Syntactic support for scala-like for-comprehensions[http://www.scala-lang.org/node/111] will be implemented
18
+ as a sequence of calls to #flat_map, #select, etc, modelling Scala's
19
+ approach[http://stackoverflow.com/questions/3754089/scala-for-comprehension/3754568#3754568].
20
+
21
+ Support for an all_catch[http://www.scala-lang.org/archives/downloads/distrib/files/nightly/docs/library/scala/util/control/Exception$.html]
22
+ idiom will be implemented to turn blocks which might throw exceptions into Option or Either
23
+ results. If this proves useful (and a good fit for Ruby), then more narrow functional catchers can be implemented as well.
24
+
25
+ == Usage
26
+
27
+ You can transform possibly nil values in a functional fashion, which many find more clear and elegant:
28
+
29
+ require 'date'
30
+ require 'time'
31
+ require 'rumonade'
32
+
33
+ def format_date_in_march(time_or_date_or_nil)
34
+ Option(time_or_date_or_nil).
35
+ map(&:to_date).select {|d| d.month == 3}.
36
+ map(&:to_s).map {|s| s.gsub('-', '')}.get_or_else("not in march!")
37
+ end
38
+
39
+ format_date_in_march(nil) # => "not in march!"
40
+ format_date_in_march(Time.parse('2011-01-01 12:34')) # => "not in march!"
41
+ format_date_in_march(Time.parse('2011-03-21 12:34')) # => "20110321"
15
42
 
43
+ (more examples coming soon...)
44
+
45
+ == Approach
46
+
47
+ There have been many[http://moonbase.rydia.net/mental/writings/programming/monads-in-ruby/00introduction.html]
48
+ posts[http://pretheory.wordpress.com/2008/02/14/the-maybe-monad-in-ruby/]
49
+ and[http://www.valuedlessons.com/2008/01/monads-in-ruby-with-nice-syntax.html]
50
+ discussions[http://stackoverflow.com/questions/2709361/monad-equivalent-in-ruby]
51
+ about monads in Ruby, which have sparked a number of approaches.
52
+
53
+ Rumonade wants to be a practical drop-in Monad solution that will fit well into the Ruby world.
54
+
55
+ The priorities for Rumonade are:
56
+ 1. Practical usability in day-to-day Ruby
57
+ * <b>don't</b> mess up normal idioms of the language (e.g., Array#map)
58
+ * <b>don't</b> slow down normal idioms of the language (e.g., Array#map)
59
+ 2. Rubyish-ness of usage
60
+ * Monad is a mix-in, requiring methods self.unit and #bind be implemented by target classes
61
+ * Prefer blocks to lambda/Procs where possible
62
+ 3. Equivalent idioms to Scala where possible
63
+
64
+ == Status
65
+
66
+ This code is in a very early state, but the Option monad is already present.
67
+ Please try it out, and let me know what you think!
@@ -1,5 +1,5 @@
1
1
  # Adapted from http://stackoverflow.com/questions/2709361/monad-equivalent-in-ruby
2
- class LazyIdentity
2
+ class LazyIdentity # :nodoc:
3
3
  def initialize(lam = nil, &blk)
4
4
  @lazy = lam || blk
5
5
  @lazy.is_a?(Proc) || raise(ArgumentError, "not a Proc")
@@ -1,10 +1,15 @@
1
1
  module Rumonade
2
- # TODO: Document this
2
+ # Mix-in for common monad functionality dependent on implementation of monadic methods unit and bind
3
+ #
4
+ # Notes:
5
+ # * classes should include this module AFTER defining the monadic methods unit and bind
6
+ #
3
7
  module Monad
4
- METHODS_TO_REPLACE = [:flat_map, :flatten]
8
+ METHODS_TO_REPLACE = [:flat_map, :flatten] # :nodoc:
5
9
 
6
- def self.included(base)
10
+ def self.included(base) # :nodoc:
7
11
  base.class_eval do
12
+ # optimization: replace flat_map with an alias for bind, as they are identical
8
13
  alias_method :flat_map_with_monad, :bind
9
14
 
10
15
  # force only a few methods to be aliased to monad versions; others can stay with native or Enumerable versions
@@ -17,20 +22,43 @@ module Rumonade
17
22
 
18
23
  include Enumerable
19
24
 
20
- def map(lam = nil, &blk)
21
- bind { |v| (lam || blk).call(v) }
25
+ # Applies the given procedure to each element in this monad
26
+ def each(lam = nil, &blk)
27
+ bind { |v| (lam || blk).call(v) }; nil
22
28
  end
23
29
 
24
- def each(lam = nil, &blk)
25
- map(lam || blk); nil
30
+ # 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)
32
+ bind { |v| self.class.unit((lam || blk).call(v)) }
26
33
  end
27
34
 
28
- def shallow_flatten
29
- bind { |x| x.is_a?(Monad) ? x : self.class.unit(x) }
35
+ # Returns the results of applying the given function to each element in this monad
36
+ def flat_map_with_monad(lam = nil, &blk)
37
+ bind(lam || blk)
30
38
  end
31
39
 
40
+ # Returns a monad whose elements are the ultimate (non-monadic) values contained in all nested monads
41
+ #
42
+ # [1] == [[Some(Some(1)), Some(Some(None))], [None]].flatten
43
+ # => true
44
+ #
32
45
  def flatten_with_monad
33
46
  bind { |x| x.is_a?(Monad) ? x.flatten_with_monad : self.class.unit(x) }
34
47
  end
48
+
49
+ # Returns a monad whose elements are all those elements of this monad for which the given predicate returned true
50
+ def select(lam = nil, &blk)
51
+ bind { |x| (lam || blk).call(x) ? self.class.unit(x) : self.class.empty }
52
+ end
53
+ alias_method :find_all, :select
54
+
55
+ # Returns a monad whose elements are the values contained in the first level of nested monads
56
+ #
57
+ # This method is equivalent to the Scala flatten call (single-level flattening), whereas #flatten is in keeping
58
+ # with the native Ruby flatten calls (multiple-level flattening).
59
+ #
60
+ def shallow_flatten
61
+ bind { |x| x.is_a?(Monad) ? x : self.class.unit(x) }
62
+ end
35
63
  end
36
64
  end
@@ -2,54 +2,92 @@ require 'singleton'
2
2
  require 'rumonade/monad'
3
3
 
4
4
  module Rumonade # :nodoc:
5
- # TODO: Document this
5
+ # Represents optional values. Instances of Option are either an instance of Some or the object None.
6
+ #
7
+ # The most idiomatic way to use an Option instance is to treat it as a collection or monad
8
+ # and use map, flat_map, select, or each:
9
+ #
10
+ # name = Option(params[:name])
11
+ # upper = name.map(&:strip).select { |s| s.length != 0 }.map(&:upcase)
12
+ # puts upper.get_or_else("")
13
+ #
14
+ # Note that this is equivalent to
15
+ #
16
+ # # TODO: IMPLEMENT FOR COMPREHENSIONS
17
+ # # see http://stackoverflow.com/questions/1052476/can-someone-explain-scalas-yield
18
+ # val upper = for {
19
+ # name <- Option(params[:name])
20
+ # trimmed <- Some(name.strip)
21
+ # upper <- Some(trimmed.upcase) if trimmed.length != 0
22
+ # } yield upper
23
+ # puts upper.get_or_else("")
24
+ #
25
+ # Because of how for comprehension works, if None is returned from params#[], the entire expression results in None
26
+ # This allows for sophisticated chaining of Option values without having to check for the existence of a value.
27
+ #
28
+ # A less-idiomatic way to use Option values is via direct comparison:
29
+ #
30
+ # name_opt = params[:name]
31
+ # case name_opt
32
+ # when Some
33
+ # puts name_opt.get.strip.upcase
34
+ # when None
35
+ # puts "No name value"
36
+ # end
37
+ #
6
38
  class Option
7
39
  class << self
40
+ # Returns a new Option containing the given value
8
41
  def unit(value)
9
42
  Rumonade.Option(value)
10
43
  end
11
44
 
45
+ # Returns the empty Option (None)
12
46
  def empty
13
47
  None
14
48
  end
15
49
  end
16
50
 
17
- def initialize
51
+ def initialize # :nodoc:
18
52
  raise(TypeError, "class Option is abstract; cannot be instantiated") if self.class == Option
19
53
  end
20
54
 
55
+ # Returns None if None, or the result of executing the given block or lambda on the contents if Some
21
56
  def bind(lam = nil, &blk)
22
- f = lam || blk
23
- empty? ? self : f.call(value)
57
+ empty? ? self : (lam || blk).call(value)
24
58
  end
25
59
 
26
60
  include Monad
27
61
 
62
+ # Returns +true+ if None, +false+ if Some
28
63
  def empty?
29
64
  raise(NotImplementedError)
30
65
  end
31
66
 
67
+ # Returns contents if Some, or raises NoSuchElementError if None
32
68
  def get
33
69
  if !empty? then value else raise NoSuchElementError end
34
70
  end
35
71
 
72
+ # Returns contents if Some, or given value or result of given block or lambda if None
36
73
  def get_or_else(val_or_lam = nil, &blk)
37
74
  v_or_f = val_or_lam || blk
38
75
  if !empty? then value else (v_or_f.respond_to?(:call) ? v_or_f.call : v_or_f) end
39
76
  end
40
77
 
78
+ # Returns contents if Some, or +nil+ if None
41
79
  def or_nil
42
80
  get_or_else(nil)
43
81
  end
44
82
  end
45
83
 
46
- # TODO: Document this
84
+ # Represents an Option containing a value
47
85
  class Some < Option
48
86
  def initialize(value)
49
87
  @value = value
50
88
  end
51
89
 
52
- attr_reader :value
90
+ attr_reader :value # :nodoc:
53
91
 
54
92
  def empty?
55
93
  false
@@ -64,7 +102,7 @@ module Rumonade # :nodoc:
64
102
  end
65
103
  end
66
104
 
67
- # TODO: Document this
105
+ # Represents an Option which is empty, accessed via the constant None
68
106
  class NoneClass < Option
69
107
  include Singleton
70
108
 
@@ -81,21 +119,21 @@ module Rumonade # :nodoc:
81
119
  end
82
120
  end
83
121
 
84
- # TODO: Document this
122
+ # Exception raised on attempts to access the value of None
85
123
  class NoSuchElementError < RuntimeError; end
86
124
 
87
- # TODO: Document this
125
+ # Returns an Option wrapping the given value: Some if non-nil, None if nil
88
126
  def Option(value)
89
127
  value.nil? ? None : Some(value)
90
128
  end
91
129
 
92
- # TODO: Document this
130
+ # Returns a Some wrapping the given value, for convenience
93
131
  def Some(value)
94
132
  Some.new(value)
95
133
  end
96
134
 
97
- # TODO: Document this
98
- None = NoneClass.instance
135
+ # The single global instance of NoneClass, representing the empty Option
136
+ None = NoneClass.instance # :doc:
99
137
 
100
138
  module_function :Option, :Some
101
139
  public :Option, :Some
@@ -1,3 +1,3 @@
1
1
  module Rumonade
2
- VERSION = "0.1.0"
2
+ VERSION = "0.1.1"
3
3
  end
data/test/option_test.rb CHANGED
@@ -63,7 +63,7 @@ class OptionTest < Test::Unit::TestCase
63
63
  end
64
64
 
65
65
  def test_map_behaves_correctly
66
- assert_equal "FOO", Some("foo").map { |s| s.upcase }
66
+ assert_equal Some("FOO"), Some("foo").map { |s| s.upcase }
67
67
  assert_equal None, None.map { |s| s.upcase }
68
68
  end
69
69
 
@@ -102,4 +102,10 @@ class OptionTest < Test::Unit::TestCase
102
102
  assert_equal [1], Some(1).to_a
103
103
  assert_equal [], None.to_a
104
104
  end
105
- end
105
+
106
+ def test_select_behaves_correctly
107
+ assert_equal Some(1), Some(1).select { |n| n > 0 }
108
+ assert_equal None, Some(1).select { |n| n < 0 }
109
+ assert_equal None, None.select { |n| n < 0 }
110
+ end
111
+ end
metadata CHANGED
@@ -2,7 +2,7 @@
2
2
  name: rumonade
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease:
5
- version: 0.1.0
5
+ version: 0.1.1
6
6
  platform: ruby
7
7
  authors:
8
8
  - Marc Siegel
@@ -10,7 +10,7 @@ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
12
 
13
- date: 2011-09-17 00:00:00 Z
13
+ date: 2011-09-20 00:00:00 Z
14
14
  dependencies: []
15
15
 
16
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.