-
-
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 | |
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.
This reminds me of:
https://github.com/aanand/do_notation/blob/master/spec/monad_plus_spec.rb
@markburns it should. Good call. I'd also like to see what @raganwald thinks, given andand.
@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
Yes, I do. Both both are relevant and interesting here. :)
@steveklabnik haha. Okay. Sorry.
/me perpetually underestimates people
No worries whatsoever.
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. :)
|
is already a Ruby thing, though...
def | other
"We can define this how we like"
end
"asdf" | "qwer"
# => "We can define this how we like"
Sure, but that doesn't mean that people won't find it really confusing.
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)
On the other hand, I could just have a big "container" object:
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...