Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save yorickpeterse/8a45e11f2bca8e0b8642 to your computer and use it in GitHub Desktop.
Save yorickpeterse/8a45e11f2bca8e0b8642 to your computer and use it in GitHub Desktop.
Simple Maybe monad in Ruby (copied straight from Git, too lazy to gemify for now)
From a31eb2863043937881e51cffb544ef9a641a62af Mon Sep 17 00:00:00 2001
From: Yorick Peterse <SNIP>
Date: Thu, 12 Mar 2015 11:08:46 +0100
Subject: [PATCH] Added a simple Maybe monad.
This is a rather simple implementation of the Maybe monad [1] / Option type [2].
It only provides the absolute bare-minimum instead of providing a fully blown
functional programming library.
Using this class allows you to turn code such as this:
value = some_array[0] ? some_array[0]['some_hash_key'] || 0.0 : 0.0
into this:
value = some_array[0].maybe('some_hash_key').or(0.0)
Or if "some_array" can also be nil:
value = some_array.maybe(0).maybe('some_hash_key').or(0.0)
Without the Maybe class you'd have to write this as such (more or less):
if some_array
value = some_array[0] ? some_array[0]['some_hash_key'] || 0.0 : 0.0
else
value = 0.0
end
The above _could_ be cleaned up a bit, but it wouldn't be much better:
if some_array
value = some_array[0] ? some_array[0]['some_hash_key'] : nil
else
value = nil
end
value ||= 0.0
The "maybe" method is available on all Ruby objects (it's defined as
Object#maybe). A few examples:
[10, 20, 30].maybe(0).or('nope') # => 10
[10, 20, 30].maybe(3).or('nope') # => 'nope'
[10, 20, 30].maybe { |array| array[0] }.or('nope') # => 10
[10, 20, 30].maybe { |array| array[3] }.or('nope') # => 'nope'
Note that this class is still very much an experiment. If it turns out not to
work for us we can just remove it and revert back to using puny if/else
statements.
[1]: http://en.wikipedia.org/wiki/Monad_(functional_programming)#The_Maybe_monad
[2]: http://en.wikipedia.org/wiki/Option_type
---
lib/reports_daemon/maybe.rb | 137 ++++++++++++++++++++++++++++++
lib/reports_daemon/maybe/pollute.rb | 25 ++++++
spec/reports_daemon/maybe/pollute_spec.rb | 31 +++++++
spec/reports_daemon/maybe_spec.rb | 89 +++++++++++++++++++
4 files changed, 282 insertions(+)
create mode 100644 lib/reports_daemon/maybe.rb
create mode 100644 lib/reports_daemon/maybe/pollute.rb
create mode 100644 spec/reports_daemon/maybe/pollute_spec.rb
create mode 100644 spec/reports_daemon/maybe_spec.rb
diff --git a/lib/reports_daemon/maybe.rb b/lib/reports_daemon/maybe.rb
new file mode 100644
index 0000000..352e589
--- /dev/null
+++ b/lib/reports_daemon/maybe.rb
@@ -0,0 +1,137 @@
+##
+# The Maybe class is a very simple implemention of the maybe monad/option type
+# often found in functional programming languages. To explain the use of this
+# class, lets take at the following code:
+#
+# pairs = [{:a => 10}]
+#
+# number = pairs[0][:a]
+#
+# While this code is very simple, there are already a few problems that can
+# arise:
+#
+# 1. If the pairs Array is empty we'll get an "undefined method [] for nil"
+# error as pairs[0] in this case returns nil.
+# 2. If the :a key is not set we'll again end up with a nil value, which might
+# break code later on.
+#
+# So lets add some code to take care of this, ensuring we _always_ have a
+# number:
+#
+# pairs = [{:a => 10}]
+#
+# number = pairs[0] ? pairs[0][:a] || 0 : 0
+#
+# Alternatively:
+#
+# pairs = [{:a => 10}]
+#
+# number = (pairs[0] ? pairs[0][:a] : nil) || 0
+#
+# Both cases are quite messy. Using the Maybe class we can write this as
+# following instead:
+#
+# pairs = [{:a => 10}]
+#
+# numbers = Maybe.new(pairs[0]).maybe(:a).or(0)
+#
+# If we use the patch for Object (adding `Object#maybe`) we can write this as
+# following:
+#
+# pairs = [{:a => 10}]
+#
+# numbers = pairs[0].maybe(:a).or(0)
+#
+# Boom, we no longer need to rely on ternaries or the `||` operator to ensure we
+# always have a default value.
+#
+class Maybe
+ ##
+ # @param [Mixed] wrapped
+ #
+ def initialize(wrapped)
+ @wrapped = wrapped
+ end
+
+ ##
+ # @return [Mixed]
+ #
+ def unwrap
+ return @wrapped
+ end
+
+ ##
+ # Retrieves a value from the wrapped object. This method can be used in two
+ # ways:
+ #
+ # 1. Using a member (e.g. Array index or Hash key) in case the underlying
+ # object defines a "[]" method.
+ #
+ # 2. Using a block which takes the wrapped value as its argument.
+ #
+ # For example, to get index 0 from an Array and return it as a Maybe:
+ #
+ # Maybe.new([10, 20, 30]).maybe(0) # => #<Maybe:0x0...>
+ #
+ # To get a Hash key:
+ #
+ # Maybe.new({:a => 10}).maybe(:a) # => #<Maybe:0x00...>
+ #
+ # Using a block:
+ #
+ # Maybe.new([10, 20, 30]).maybe { |array| array[0] }
+ #
+ # The block is only evaluated if the current Maybe wraps a non-nil value. The
+ # return value of the block is wrapped in a new Maybe instance.
+ #
+ # @param [Mixed] member
+ #
+ # @yieldparam [Mixed] wrapped The wrapped value
+ #
+ # @return [Maybe]
+ #
+ def maybe(member = nil)
+ if member
+ value = brackets? ? @wrapped[member] : nil
+ elsif @wrapped
+ value = yield @wrapped
+ end
+
+ if value.nil? and @wrapped.nil?
+ # No point in allocating a new Maybe for this.
+ return self
+ else
+ return Maybe.new(value)
+ end
+ end
+
+ ##
+ # Returns the wrapped value or returns a default value, specified as either an
+ # argument to this method or in the provided block.
+ #
+ # A simple example:
+ #
+ # Maybe.new([10, 20]).maybe(0).or(9000)
+ #
+ # And using a block:
+ #
+ # Maybe.new([10, 20]).maybe(0).or { 9000 }
+ #
+ # @param [Mixed] default
+ # @return [Mixed]
+ #
+ def or(default = nil)
+ return @wrapped unless @wrapped.nil?
+ return default unless default.nil?
+ return yield
+ end
+
+ private
+
+ ##
+ # @return [TrueClass|FalseClass]
+ #
+ def brackets?
+ return @wrapped.respond_to?(:[])
+ end
+end # Maybe
diff --git a/lib/reports_daemon/maybe/pollute.rb b/lib/reports_daemon/maybe/pollute.rb
new file mode 100644
index 0000000..80f27b9
--- /dev/null
+++ b/lib/reports_daemon/maybe/pollute.rb
@@ -0,0 +1,25 @@
+class Object
+ ##
+ # Wraps the current object in a Maybe instance, optionally calling {Maybe#get}
+ # if an argument or block is given.
+ #
+ # @example
+ # [10, 20, 30].maybe(0).or(50) # => 10
+ # [10, 20, 30].maybe { |arr| arr[3] }.or(50) # => 50
+ #
+ # @param [Mixed] member
+ #
+ # @see [Maybe#get]
+ #
+ def maybe(member = nil)
+ retval = Maybe.new(self)
+
+ if member
+ retval = retval.maybe(member)
+ elsif block_given?
+ retval = retval.maybe(&Proc.new)
+ end
+
+ return retval
+ end
+end # Object
diff --git a/spec/reports_daemon/maybe/pollute_spec.rb b/spec/reports_daemon/maybe/pollute_spec.rb
new file mode 100644
index 0000000..d2d4372
--- /dev/null
+++ b/spec/reports_daemon/maybe/pollute_spec.rb
@@ -0,0 +1,31 @@
+require 'spec_helper'
+
+describe Object do
+ describe '#maybe' do
+ describe 'without an argument' do
+ it 'returns a Maybe' do
+ obj = described_class.new
+
+ obj.maybe.should be_an_instance_of(Maybe)
+ end
+ end
+
+ describe 'with an argument' do
+ it 'returns a Maybe wrapping the return value of Maybe#maybe' do
+ maybe = [10].maybe(0)
+
+ maybe.should be_an_instance_of(Maybe)
+ maybe.unwrap.should == 10
+ end
+ end
+
+ describe 'with a block' do
+ it 'returns a Maybe wrapping the return value of the block' do
+ maybe = [10].maybe { |array| array[0] }
+
+ maybe.should be_an_instance_of(Maybe)
+ maybe.unwrap.should == 10
+ end
+ end
+ end
+end
diff --git a/spec/reports_daemon/maybe_spec.rb b/spec/reports_daemon/maybe_spec.rb
new file mode 100644
index 0000000..4891612
--- /dev/null
+++ b/spec/reports_daemon/maybe_spec.rb
@@ -0,0 +1,89 @@
+require 'spec_helper'
+
+describe Maybe do
+ describe '#unwrap' do
+ it 'returns the wrapped object' do
+ described_class.new(10).unwrap.should == 10
+ end
+ end
+
+ describe '#maybe' do
+ describe 'using an argument' do
+ describe 'with a wrapped object defining the #[] method' do
+ it 'returns a Maybe wrapping the return value' do
+ initial = described_class.new([10])
+ maybe = initial.maybe(0)
+
+ maybe.should be_an_instance_of(described_class)
+ maybe.unwrap.should == 10
+ end
+
+ it 'returns a Maybe wrapping nil if nil is returned' do
+ initial = described_class.new([])
+ maybe = initial.maybe(0)
+
+ maybe.should be_an_instance_of(described_class)
+ maybe.unwrap.should == nil
+ end
+ end
+
+ describe 'with a wrapped object without the #[] method' do
+ it 'returns a Maybe wrapping nil' do
+ initial = described_class.new(Object.new)
+ maybe = initial.maybe(0)
+
+ maybe.should be_an_instance_of(described_class)
+ maybe.unwrap.should == nil
+ end
+ end
+
+ it 'returns self when the wrapped and returned values are nil' do
+ initial = described_class.new(nil)
+
+ initial.maybe(0).should === initial
+ end
+ end
+
+ describe 'using a block' do
+ it 'returns a Maybe wrapping the return value of the block' do
+ maybe = described_class.new(10).maybe { |val| val }
+
+ maybe.should be_an_instance_of(described_class)
+ maybe.unwrap.should == 10
+ end
+
+ it 'returns a Maybe wrapping nil if the block returns nil' do
+ maybe = described_class.new(10).maybe { nil }
+
+ maybe.should be_an_instance_of(described_class)
+ maybe.unwrap.should == nil
+ end
+
+ it 'returns self when the wrapped and returned values are nil' do
+ initial = described_class.new(nil)
+
+ initial.maybe { nil }.should === initial
+ end
+ end
+ end
+
+ describe '#or' do
+ describe 'with a Maybe wrapping a non-nil object' do
+ it 'returns the wrapped object' do
+ described_class.new(10).or.should == 10
+ end
+ end
+
+ describe 'with an argument and with a Maybe wrapping nil' do
+ it 'returns the given argument' do
+ described_class.new(nil).or(10).should == 10
+ end
+ end
+
+ describe 'without an argument and with a Maybe wrapping nil' do
+ it 'yields the given block' do
+ described_class.new(nil).or { 10 }.should == 10
+ end
+ end
+ end
+end
--
2.3.2
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment