Baby's first screencast 9

Posted by Piers Cawley Fri, 14 Mar 2008 19:29:00 GMT

If you follow the Ruby blogs, you will probably have seen a bunch of programmers attempting to do something akin to Haskell’s maybe, or the ObjectiveC style, message eating null.

Generally, by about the 3rd time you’ve written

if foo.nil? ? nil : foo.bar 
  ...
end

you’re getting pretty tired of it. Especially when foo is a variable you’ve had to introduce solely to hold a value while you check that it’s not nil. The pain really kicks in when you really want to call foo.bar.baz. You can end up writing monstrosities like (tmp = foo.nil? ? nil : foo.bar).nil? ? nil : tmp.baz (actually, if you were to write that in production code, you probably have bigger problems). One option is to just define NilClass#method_missing to behave like its Objective C equivalent, but I’ve never quite had the nerve to find out how that might work. I wanted to write

if maybe { foo.bar.baz }
  ...
end

and have nil behave like an Objective C nil for the duration of the block, but no longer. So I wrote it. Then I thought about how to present it. I wrote the thing test first using rspec and the whole thing just flowed, but writing up a test first development process for a blog entry is painful, so I’ve made a very rough (but blessedly short) screencast of the process instead.

That’s a slightly reduced thumbnail, the movie is substantially more readable. The bottom pane of the window is the output of autotest rerunning the spec every time either the spec or the implementation changes. The top pane alternates between the specs and the implementation. Generally, every time I edit the specs, a test starts failing and every time I edit the implementation it starts passing again. In the (any) real coding run, there were of course false starts, but generally the specs kept me pretty straight.

A word or two of warning: This is a completely unedited, silent, screen cast, there are typos, backtrackings and other embarrassments. I stopped recording once I’d got 4 tests passing, but this is far from release quality (it’s perfectly usable if you know its limitations, but it’s not entirely robust).

Please let me know what you think of this. I’m aiming to make a more polished version, complete with voice over and it would be good to know which bits are confusing and need addressing in more detail in the voice over.

Comments

Leave a response

  1. Avatar
    James Abley about 3 hours later:

    That’s quite cool. I’m almost inspired to pick up Ruby in more depth, since that uses some language features that I’ve not gone deep enough to encounter yet. I look forward to seeing a more in-depth version.

    And don’t worry about the typos, I’ve got far worse fat fingers than that!

  2. Avatar
    Piers Cawley about 16 hours later:

    What I’d really like to do is to write:

    class Whatever
      use :laissez_faire_nils
    
      ...
    end

    and have all the methods I define in that class definition use an Objective C style nil. For bonus points, if someone reopens the class, they shouldn’t get that style of nils unless they explicitly ask for them again. However, I think the only way to do something like that would be to use an explicit block:

    class Whatever
      with(:laissez_faire_nils) {
        def some_method
          ...
        end
      }
    end

    Mutter… {} isn’t right there. But then wrapping do ... end around a block of purely declarative code just feels ugly too. If only there were some way of implementing new block methods, so we could write:

    with(:whatever)
      ...
    end

    Lisp style macros rock…

  3. Avatar
    Kevin Ballard about 20 hours later:

    I’m a fan of try(:method), as in

    foo.try(:bar).try(:baz)

    It’s also really simple to implement

    class Object
      def try(name, *args)
        method(name).call *args
      end
    end
    
    class NilClass
      def try(name, *args)
        nil
      end
    end
  4. Avatar
    Aristotle Pagaltzis about 21 hours later:

    Piers, I don’t think that code is right. Your modification of @nil@’s behaviour is temporally scoped, whereas it should be lexically scoped.

  5. Avatar
    Tom Moertel 1 day later:

    You might want to reconsider the requirement that “maybe(3) {nil} should be nil,” especially if you’re using the Maybe monad as inspiration. Without that requirement, the semantics of maybe(z_) {_e} are straightforward: nil represents failure.

    If you add that requirement, however, failure is represented by attempting to call a non-existent method on a nil value. Such failure semantics result in unintuitive inconsistencies. Consider, for example, the following chain:

    maybe(3) { e1.e2.e3.e4 }

    If e1, e2, or e3 fails (i.e., returns nil), the entire computation e1.e2.e3.e4 fails and thus the result of the maybe computation is 3. If, however, only e4 fails, the entire computation e1.e2.e3.e4 somehow “succeeds” (with the result of nil), which seems inconsistent. Why should nil denote failure for all but the last element of the chain?

    It seems, then, that the requirement ought to be “maybe(3) {nil} should be 3”.

    Cheers,
    Tom

  6. Avatar
    Piers Cawley 2 days later:

    @Aristotle: Ugh! You’re right. I have the awful feeling that the only successful fix would involve changing the interface so that the first value is passed as the value to the block:

    maybe(initial_value, default) {|it|
      it.should.return.the.default.if.nil.crops.up
    end

    That way we can wrap the initial value in a proxy object before the block gets at it and wrap each intermediate value as well. I don’t like the interface so much though (even though it would end up working even more like a Haskell Maybe).

    @Tom: Ah yes, point taken.

  7. Avatar
    Daniel Berger 3 days later:

    foo.bar.baz rescue nil

    Have a nice day.

  8. Avatar
    Piers Cawley 3 days later:

    @Daniel: If you want to shoot yourself in the foot, go ahead and do that. If, however, you’ve been there, done that and got the holes to prove it, you start wanting something a little more robust.

  9. Avatar
    loki 29 days later:

    May be you could use:

    if defined? foo.bar
      puts 'May be'
    end
      
Comments