rumonade 0.1.0 → 0.1.1

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