More Prevention of NoMethodErrors on nil
Geplaatst door Michiel de Mare wo, 05 maa 2008 06:24:00 GMT
What a sudden interest in avoiding NoMethodErrors on nil lately! I’ve just thought of another method. (Remember, you can solve any problem by adding a layer of indirection.)
Say you have Remco’s canonical example: person.name.upcase
Both person
and person.name
may be nil. So we usually write: person && person.name && person.name.upcase
.
rrss
? I’ve introduced
it last month. It stands for “returns result, same self”. Its definition is beyond trivial:
class Object
def rrss
yield self
end
end
With rrss
we can rewrite the original as:
person.rrss {|p| p.name}.rrss {|n| n.upcase}
Or with to_proc
notation:
person.rrss(&:name).rrss(&:upcase)
.
We still need to check for nil though:
person.rrss {|p| p && p.name}.
rrss {|n| n && n.upcase}
Doesn’t this last bit look like it’s in need of a bit of refactoring? Its lack of DRY-ness jumps right out of the page.
First try:
x = lambda {|p| p && p.name}
y = lambda {|n| n && n.upcase}
person.rrss(&x).rrss(&y)
Let’s use an array instead!
arr = [lambda {|p| p && p.name},
lambda {|n| n && n.upcase}]
arr.inject(person) {|o,l| o.rrss(&l) }
We should use the nil test to within the inject block.
arr = [lambda {|p| p.name},
lambda {|n| n.upcase}]
arr.inject(person) {|o,l| o && o.rrss(&l) }
Hey, can’t we write those lambdas a lot nicer?
arr = [lambda(&:name), lambda(&:upcase)]
arr.inject(person) {|o,l| o && o.rrss(&l) }
Do we even need lambdas? We’re calling them with the ampersand(&),
so to_proc
will be called on them anyway.
arr = [:name,:upcase]
arr.inject(person) {|o,l| o && o.rrss(&l) }
Let’s turn this into a method!
class Object
def chain(*a)
a.inject(self) {|o,l| o && o.rrss(&l) }
end
end
We’ve made quite a journey. But look, we’ve come a long way!
# long ago
person && person.name && person.name.upcase
# now
person.chain(:name,:upcase)
# with obedient nil
# (in alternate, bugless universe)
person.name.upcase
Why don’t we go a little further yet. What if we’re not worried about nil
.
What if the want to stop a calculation as soon as it’s zero?
result = if value == 0 then value else
tmp = calc(value)
if tmp == 0 then tmp else
more_calc(tmp)
end
end
We could write another method on object to handle just this. But let’s make it more generic. Let’s accept a block.
class Object
def chain(*arr)
arr.inject(self) do |o,lmb|
if block_given?
yield o,lmb
else
o && o.rrss(&lmb)
end
end
end
end
How does this work in practice? First, we need to define the chain, and
Symbol#to_proc
won’t cut it anymore. We’ll need lambdas.
chain = [lambda {|v| calc(v) },
lambda {|v| more_calc(v)}]
Lambdas are ugly, but even so, that wasn’t so bad. Now, let’s use it!
value.chain(*chain) do |v,lmb|
v == 0 ? v : lmb[v]
end
Of course, the more steps you have, the bigger the gains of this method. With two steps, it’s a toss up. With three of more steps, you can go a long way in cleaning up your code.
Chain is DEFINITELY a monad. It seems we Rubyists are doomed to reinvent Haskell as well as Smalltalk and Lisp.
But it is interesting to evolve our understanding through use and to discover why certain features exist in languages like Lisp and Haskell.
Sure it’s a monad. But “monad” sounds scary! They need better marketing, and dropping the name seems a useful first step.
Also, I think that Haskell’s static types distract from the essence from monads, which is in my view adding a layer of indirection between chained method invocations.
Hi,
I don’t understand, what’s wrong with just redefining method_missing on NilClass?
People often call a method on what they thought was an object but what turned out to be
nil
. This is the single most common source of bugs.This kind of bug can be caught because the method call is intercepted by
NilClass#method_missing
. This method raises an exception with a useful message.By redefining
method_missing
you give up this extremely useful diagnostic message.Ah, I see.
Isn’t it possible (this may sound ridiculous, but I’m no expert) to use something like person.chain.name.upcase by making the chain method return a subclass of NilClass which isn’t “whiny”?
You can get rid of the lambdas with this:
Which lets you write:
Probably crazy block scope bugs, but interesting anyway.
Reg is right: the block
is similar the bind rule for the Maybe monad. (I say “similar” because if you want to more fully adhere to Maybe-monad semantics, you’ll need to make sure that
false
is not mistaken for a “nothing” value:You’ll also need to bar side effects to be able to satisfy the monad laws.)
In effect, we are defining
nil
to represent the result of a computation that returns nothing (fails), and we’re defining any non-nil value to represent the result of a computation that succeeds in producing a value. Then our bind rule takes the result o of a previous computation (successful or failed) and a method l that, given a successfully produced value, computes a new value (or itself fails). From this, bind generates a new computation that:A chain of such computations has the nice property that the chain will stop evaluating subsequent l methods after the first failure. (In a non-strict language, it’s easy to make the entire computation halt after the first failure.)
Cheers,
Tom