Skip to content

Instantly share code, notes, and snippets.

@pithyless
Created March 27, 2012 14:44
Show Gist options
  • Star 13 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • 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
@pithyless
Copy link
Author

Would you use this? Should I package it up in a gem?

What should the >> be called?

@steveklabnik
Copy link

I would implement this in a style similar to Rack middlewares:

result = Either.new do
  chain DoSomeStuff
  chain FinickyThirdParty
end

@steveklabnik
Copy link

... that's interface wise, anyway. I do think this has some utility.

@steveklabnik
Copy link

I've been meaning to write 'rack is a monad' for a while. :p

@pithyless
Copy link
Author

Only problem with the chain interface is no applicative possibility:

res1 = OneThing
res2 = OtherThing

(res1 & res2) >> proc { Merge.call(res1, res2) }

With chaining only, res2 would need to know about res1.

@steveklabnik
Copy link

mmmmmmmm there's a way to make this work. Applicative is just a weaker monad, after all...

@pithyless
Copy link
Author

On the other hand, I could just have a big "container" object:

result = Either.new do
  chain Transaction.new
  chain DoSomeStuff
  chain FinickyThirdParty
end

Anything that does something interesting that the next steps may need, could add it to the Transaction object. It is after all, being executed in the context of a specific "Transaction", however you may define that. Although this seems kind of weak...

@steveklabnik
Copy link

That shouldn't be needed. You may need to build <- as well as >>=, which is pretty trivial.

result = Either.new do
    let x, DoSomeStuff
    chain FinickyThirdParty
end

? Or something. Point is, it's totally do-able.

@markburns
Copy link

@steveklabnik
Copy link

@markburns it should. Good call. I'd also like to see what @raganwald thinks, given andand.

@rbxbx
Copy link

rbxbx commented Mar 27, 2012

@steveklabnik you realize aanand and andand are two different things, yes?

One (@aanand) being the gentleman who ported do_notation to Ruby and the latter (andand) which is a Ruby (sort of) port of the Maybe monad by @raganwald

@steveklabnik
Copy link

Yes, I do. Both both are relevant and interesting here. :)

@rbxbx
Copy link

rbxbx commented Mar 27, 2012

@steveklabnik haha. Okay. Sorry.

/me perpetually underestimates people

@steveklabnik
Copy link

No worries whatsoever.

@markburns
Copy link

@rbxbx, I think it's a fair point. I do wonder whether or not @aanand was born with that name or changed it just for occasions such as this discussion.

@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