Skip to content

Instantly share code, notes, and snippets.

@pithyless
Created March 27, 2012 14:44
Show Gist options
  • Save pithyless/2216519 to your computer and use it in GitHub Desktop.
Save pithyless/2216519 to your computer and use it in GitHub Desktop.
Chaining Either for Ruby
# A chainable Either monad for Ruby
#
# Examples
#
# Either.right('s') >> proc { |s| Either.right(s + '-1') } >> proc { |s| Either.right(s + '-2') }
# #=> #<Either @left=nil, @right="s-1-2">
#
# Either.right('s') >> proc { |s| Either.left('error!') } >> proc { |s| Either.right(s + '-2') }
# #=> #<Either @left='error!', @right=nil>
#
# Returns either left or right.
class Either
attr_reader :left, :right
private_class_method :new
def initialize(left, right)
@left = left
@right = right
end
def left?
!!left
end
def right?
not left?
end
def >>(callable)
if left?
self
else
callable.call(right).tap do |e|
fail "No quantum leaps allowed! Expected Either; got #{e.inspect}" unless e.is_a?(Either)
end
end
end
# Short-circuit applicative AND
#
# Examples
#
# Either.right(1) & Either.right(2) & Either.right(3)
# #=> #<Either: @left=nil, @right=3>
#
# Either.right(1) & Either.left(2) & Either.right(3)
# #=> #<Either: @left=2, @right=nil>
#
# Returns either the first Left or the last Right
def &(other)
fail "Expected Either; got #{other.inspect}" unless other.is_a?(Either)
if left?
self
else
other
end
end
def self.left(left)
new(left, nil)
end
def self.right(right)
new(nil, right)
end
end
def controller
res = ParseParam.call(params)
res = res >> lambda { |res| DoSomeStuff.call(res) }
res = res >> lambda { |res| FinickyThirdParty.call(res) }
res = res >> ...
res.to_xml
end
# If this seems like a good idea, we can add a little bit of sugar:
res = Either.right('ok')
res >>= JustAnotherProc
@therealadam
Copy link

Chaining is tricky.

  • Part of me likes using an operator for it, and part of me dislikes the explosion of inscrutable operators in Haskell and Scala. Maybe | would work, as it looks like a shell pipe?
  • The trick with let/chain is that they convert message sends into a little language that doesn't look like message sends. But, I think its similarity to Rack will make some people happy and could work well for larger operations.

In summary, I was going to say I'm kinda "eh" on this, but I've convinced myself it's possibly handy. :)

@steveklabnik
Copy link

| is already a Ruby thing, though...

@markburns
Copy link

def | other
  "We can define this how we like"
end

"asdf" | "qwer"
# => "We can define this how we like" 

@steveklabnik
Copy link

Sure, but that doesn't mean that people won't find it really confusing.

@rbxbx
Copy link

rbxbx commented Mar 27, 2012

I'm not sure the type of people you could talk into using your monad library are necessarily easily confused :)

That said, half the beauty of andand & co was that you didn't have to know you were doing anything "fancy"/"scary"

@markburns
Copy link

Good point. I guess I see this as more of an academic exercise. I'm not sure we can squeeze a whole deal more out of playing with ruby syntax without making it really confusing

@pithyless
Copy link
Author

In my defense: usually exception handling or returning nil/NullObject is enough, and I'm not against it. This was driven by a use case where each UnitOfWork had lots of possible failure cases and I had to manage handling all of them and rendering them uniformly:

module SomeDBWork
  def self.call
    obj = SomeWorker.new
    return Either.left(obj.errors) unless obj.valid?
    ...
  end
end

module ThirdParty
  def self.call
    ...
  rescue Interwebs::Broken => e
    Either.left("Http Error! => e.message")
  end
end

controller
  res = doWork >> moreWork >> otherStuff 
  res.to_xml    # I don't care if it was success, exception, bad param, etc; Oh yea, and I need this formatted as TPS Report
end

@pithyless
Copy link
Author

@markburns, this is not really meant as an academic exercise. Rather, if I implement this, does it help the future reader focus on what the main business flow is without getting bugged down in tons of error handling.

@markburns
Copy link

@pithyless, sorry I didn't mean to belittle it. I was thinking more of my pipe overloading when I said that.

I think Steve already mentioned it, but definitely checkout avdi's exceptional ruby/confident code.
I guess maybe it could, but I wonder how you'd feel after reading/watching that material.

@pithyless
Copy link
Author

@markburns, I didn't intend to make it sound belittling ;-)

Overall, I'm just looking for a solution to the problem that is empathetic to the reader of the code. Just finished watching @avdi's talk, and I will try again to see if I can fix most of the warts by just using a more confident approach. I may just end up using some of these ideas in a more sophisticated Null / Maybe object.

Thank you all for the feedback; I'm going to attack this problem again and see if I can't work my way through it without resorting to Monads. :)

@steveklabnik
Copy link

Small note of order: you will still be using a monad, maybe just not formally. ;)

@pzol
Copy link

pzol commented Mar 28, 2012

@pzol
Copy link

pzol commented Dec 27, 2013

In this spirit I just came up with this https://github.com/pzol/deterministic (still work in progress)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment