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 +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.
|