Err the Blog Atom Feed Icon
Err the Blog
Rubyisms and Railities
  • “Sugary Adapters”
    – Chris on February 16, 2008

    Very recently, Simon Harris had an idea: nil? for Ambition. Tasty sugar.

    Let’s figure out what it takes to make

    User.select { |x| x.nil? }
    

    behave just like

    User.select { |x| x == nil }
    

    in Ambition.

    Short and Sweet

    Simon’s approach was to modify Ambition directly to add support for nil?. While this is for sure ambitious, nil? is just another method. Not special. The adapter should decide what to do with it.

    Easy. Here’s what we added to the ActiveRecord adapter’s Select translator:

    def nil?(column)
      left = "#{owner.table_name}.#{quote_column_name column}"
      negated? ? not_equal(left, nil) : self.==(left, nil)
    end
    

    See it in action on lines 84 to 87.

    The tests, of course, can be found in types_test.

    Chaining Stuffs

    So, how does this work?

    Every adapter’s Select translator has a special chained_call method. Ambition invokes chained_call and passes it an array of symbols when a chained.method.call is executed on itself.

    In this case, the chain is m.name.nil?. Ambition knows that m is itself and ignores it, passing [ :name, :nil? ] to chained_call.

    The ActiveRecord adapter’s chained_call method takes the passed array and, if it can find the second element, sends it the first element.

    Basically:

    # methods = [ :name, :nil? ]
    if respond_to? methods[1]
      send(methods[1], methods.first)
    end
    

    Which translates to:

    self.nil? :name
    

    Cool. Adapters don’t need to set themselves up this way, but it works for ActiveRecord.

    Notice: the ActiveRecord adapter doesn’t support anything more than chains two methods deep. It calls the second element and passes the first, ignoring the rest. Almost discouraging, but chin up – this is ActiveRecord specific. Ambition itself supports chains of arbitrary length, and your adapter can, too.

    So array.include?, right?

    The thing is, chained_call is only invoked when a chained method call is executed on an object Ambition owns.

    User.select { |x| x.nil? }
    

    In the above, Ambition owns the x. It’s self as far as the translator is concerned.

    User.select { |x| [1,2,3].include? x.id }
    

    Ambition does not own the array, only the x.id. So what happens?

    Well, it’s the same as [1,2,3] == x.id to Ambition. The dude really doesn’t care. Any time there is something like left op right, Ambition calls op(left, right) on your translator.

    Here’s an idea of the call:

    include?([1,2,3], x.id)
    

    Luckily x.id is translated for you prior to this. The call really looks more like:

    include?([1,2,3], 'users.id')
    

    The include? definition, then, on ActiveRecord’s translator is very straightforward:

    def include?(left, right)
      left = left.map { |element| sanitize element }.join(', ')
      "#{right} IN (#{left})"
    end
    

    Beautiful.

    Join the Fun

    While the Err twitter is great for general stuff, you should really hop on the Ambition mailing list if you want in on this action. Or just watch the project on GitHub.

    Til next time.

  • Kevin Marsh, about 6 hours later:

    Nice bookmarking support in github, that’s what this was really about, wasn’t it?

  • Scott Ballantyne, 2 months later:

    Just finding Ambition, dunno what I was doing before. Does Datamapper find inspriation from this? Some of the Ambition stuff like first and each seem like Datamapper. Sorry for stating the obvious, but thanks for you efforts.

  • Bob Aman, 2 months later:

    How hard would it be to implement Ambition without ParseTree/Ruby2Ruby?

  • Chris, 3 months later:

    Bob: Impossible!

  • Four people have commented.
    Chime in.
    Sorry, no more comments :(
This is Err, the weblog of PJ Hyett and Chris Wanstrath.
All original content copyright ©2006-2008 the aforementioned.