-
-
Save pithyless/2216519 to your computer and use it in GitHub Desktop.
# 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 | |
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"
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
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
@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.
@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.
@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. :)
Small note of order: you will still be using a monad, maybe just not formally. ;)
In this spirit I just came up with this https://github.com/pzol/deterministic (still work in progress)
Sure, but that doesn't mean that people won't find it really confusing.