Skip to content

Instantly share code, notes, and snippets.

@mmacia
Last active August 29, 2015 14:08
Show Gist options
  • Save mmacia/61d7cc6cba2ff0686e11 to your computer and use it in GitHub Desktop.
Save mmacia/61d7cc6cba2ff0686e11 to your computer and use it in GitHub Desktop.
Explaining Optional Monad for lazy developers
# get the merchant name of a product in a shopping cart
@cart.items.first.purchasable.user.merchant.first_name
# => NoMethodError (undefined method for nil:NilClass)
@cart.items.first.purchasable.user.merchant.first_name
# ^
# \------ This item not exists (disposed, destroyed, ...)
# But any method in the chain could also be nil!
# Fix #1: the hard way
if @cart
cart = @cart
if cart.items
items = cart.items
if items.first
first = items.first
if first.purchasable
purchasable = first.purchasable
if purchasable.user
user = purchasable.user
if user.merchant
merchant = user.merchans
if merchant.first_name
merchant.first_name # OMG!
end
end
end
end
end
end
end
# Fix #2: Rails way(tm)
@cart.try(:items)
.try(:first)
.try(:purchasable)
.try(:user)
.try(:merchant)
.try(:first_name)
# better, but weird ...
# #try method is defined in Rails by monkey patching each object in the system, not nice :(
# The right way to do this in OOP is using decorator pattern.
class Optional
attr_reader :value
def initialize(val)
@value = val
end
def try(*args, &block)
if value.nil?
nil
else
value.public_send(*args, &block)
end
end
end
optional_string = Optional.new('hello world')
length = optional_string.try(:length)
#=> 11
optional_string = Optional.new(nil)
length = optional_string.try(:length)
#=> nil
# Fix #3: Using Optional decorator
optional_cart = Optional.new(@cart)
optional_items = Optional.new(optional_cart.try(:items))
optional_first = Optional.new(optional_items.try(:first))
optional_purchasable = Optional.new(optional_first.try(:purchasable))
optional_user = Optional.new(optional_purchasable.try(:user))
optional_merchant = Optional.new(optional_user.try(:merchant))
optional_first_name = Optional.new(optional_merchant.try(:first_name))
optional_first_name.value
# Fully OOP compliant but clumsy ....
# let's try to add a more convenient way to use Optional ...
class Optional
def and_then(&block)
if value.nil?
Optional.new(nil)
else
block.call(value)
end
end
end
Optional.new(@cart)
.and_then { |cart| Optional.new(cart.items) }
.and_then { |items| Optional.new(items.first) }
.and_then { |first| Optional.new(first.purchasable) }
.and_then { |purchasable| Optional.new(purchasable.user) }
.and_then { |user| Optional.new(user.merchant) }
.and_then { |merchant| Optional.new(merchant.first_name) }.value
# Pretty nice!
# This code no uses monkey patching anymore, and it is conceptually clear but we can go further ...
class Optional
def method_missing(*args, &block)
and_then do |value|
Optional.new(value.public_send(*args, &block))
end
end
end
# This syntax sugar trick uses ruby reflexion to delegate any message
# to Optional#and_then method, so we can rewrite the code ...
Optional.new(@cart).items.first.purchasable.user.merchant.first_name.value
# ... which is WAY more convenient!
class Optional
attr_reader :value
def initialize(val)
@value = val
end
def and_then(&block)
if value.nil?
Optional.new(nil)
else
block.call(value)
end
end
def method_missing(*args, &block)
and_then do |value|
Optional.new(value.public_send(*args, &block))
end
end
end
# ... and THIS is a monad!
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment