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.

Now for the level of indirection. Remember 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.

Geplaatst in ,  | 7 reacties

Reacties

  1. Reg Braithwaite zei ongeveer 8 uur later:

    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.

  2. Michiel zei ongeveer 8 uur later:

    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.

  3. Ivan Vega zei ongeveer 11 uur later:

    Hi,

    I don’t understand, what’s wrong with just redefining method_missing on NilClass?

  4. Michiel zei ongeveer 12 uur later:

    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.

  5. Ivan Vega zei ongeveer 14 uur later:

    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”?

  6. Jacob Maine zei ongeveer 15 uur later:

    You can get rid of the lambdas with this:

    class Object
      def chain(*arr)
        arr.inject(self) do |o,lmb| 
          if block_given? 
            yield o, method(lmb).to_proc
          else
            o && o.rrss(&lmb)
          end
        end
      end
    end
    

    Which lets you write:

    value.chain(:calc, :more_calc) do |v,lmb| 
      v == 0 ? v : lmb[v]
    end
    

    Probably crazy block scope bugs, but interesting anyway.

  7. Tom Moertel zei 1 dag later:

    Reg is right: the block

    {|o,l| o && o.rrss(&l) }

    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:

    {|o,l| o.nil? ? nil : o.rrss(&l) }

    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:

    • fails immediately if o represents a failure;
    • fails after evaluating o.l if the evaluation results in failure; or
    • returns the successfully computed result of o.l.

    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

(Laat url/e-mail achter »)

   Voorvertoning