Last active
August 29, 2015 14:08
-
-
Save mmacia/61d7cc6cba2ff0686e11 to your computer and use it in GitHub Desktop.
Explaining Optional Monad for lazy developers
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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