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 +7 -1
- data/README.rdoc +62 -10
- data/lib/rumonade/lazy_identity.rb +1 -1
- data/lib/rumonade/monad.rb +37 -9
- data/lib/rumonade/option.rb +50 -12
- data/lib/rumonade/version.rb +1 -1
- data/test/option_test.rb +8 -2
- metadata +2 -2
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
|
-
|
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
|
-
|
8
|
-
|
9
|
-
|
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
|
-
*
|
14
|
+
* Hash
|
15
|
+
* (more TBD)
|
12
16
|
|
13
|
-
|
14
|
-
|
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!
|
data/lib/rumonade/monad.rb
CHANGED
@@ -1,10 +1,15 @@
|
|
1
1
|
module Rumonade
|
2
|
-
#
|
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
|
-
|
21
|
-
|
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
|
-
|
25
|
-
|
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
|
-
|
29
|
-
|
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
|
data/lib/rumonade/option.rb
CHANGED
@@ -2,54 +2,92 @@ require 'singleton'
|
|
2
2
|
require 'rumonade/monad'
|
3
3
|
|
4
4
|
module Rumonade # :nodoc:
|
5
|
-
#
|
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
|
-
|
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
|
-
#
|
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
|
-
#
|
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
|
-
#
|
122
|
+
# Exception raised on attempts to access the value of None
|
85
123
|
class NoSuchElementError < RuntimeError; end
|
86
124
|
|
87
|
-
#
|
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
|
-
#
|
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
|
-
#
|
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
|
data/lib/rumonade/version.rb
CHANGED
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
|
-
|
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.
|
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-
|
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.
|