Forum: Ruby Are my metaprogramming underpants showing?

Announcement (2017-05-07): www.ruby-forum.com is now read-only since I unfortunately do not have the time to support and maintain the forum any more. Please see rubyonrails.org/community and ruby-lang.org/en/community for other Rails- und Ruby-related community platforms.
31af45939fec7e3c4ed8a798c0bd9b1a?d=identicon&s=25 M.B.Smillie (Guest)
on 2005-12-06 00:54
(Received via mailing list)
Hello all,

I've done a quick little project to see if I can wrap my head around
Ruby metaprogramming, and I'd like to run it by some more-experienced
minds.

I picked the quick little task of coding up the web API for flickr
(http://flickr.com/services/API).  Not necessarily for practical
usage (though I may use it myself), but more because it struck me as
a good example since there are a lot of methods with extremely
similar behaviour on the client side (send parameters to this URL).
In any case, here's the code:

require 'pp'
class Flickr
   def method_missing(method_id, *params)
     # Find the desired class name
     class_name = method_id.to_s.capitalize
     # Find the corresponding instance variable
     ivar = :"@#{method_id}"

     # have we already made this particular class before?
     unless self.class.const_defined?(class_name)
       # new class which inherits from this one.
       new_class = self.class.const_set(class_name, Class.new
(self.class))
       # new instance variable
       instance_variable_set(ivar, new_class.new)
     end

     # if we have parameters, execute the appropriate method (returning
     # what, though?) otherwise return the instance we just made so
     # that the next thing can be called correctly.
     return instance_variable_get(ivar) unless params.length > 0

     the_method = instance_variable_get(ivar).class.name.downcase.gsub
(/::/, '.')
     # abstract out the actual API call for the moment.
     puts "call: http://flickr.com/services/rest/?method='#
{the_method}'"
     puts "with these other params: "
     pp *params
   end
end


# envisaged usage
flickr = Flickr.new
result = flickr.test.echo({"api_key" => "something long", "foo" =>
"bar"})

The idea is that the usage should mirror how the methods are defined
in the flickr docs.

In general I'm asking (like the subject suggests) if my
metaprogramming underpants are showing?  Have I defied any particular
conventions? Does this seem like a sensible approach, and if so, a
sensible implementation? Have I set myself up for some rather
spectacular failures?

Specifically, though, there are three aspects of the code that I'm
particularly curious whether anyone has any alternate approaches:

1. encoding the method name as a class heirarchy.  e.g.
'flickr.test.echo' is implicit in the class definition that results
from that call (Flickr::Test::Echo), then the method name gets
reconstructed from that when the eventual call is made.  Any other
ways to do this?

2. relying on params.length to determine the 'end' of the call seems
a little funny.  On the other hand, every method call takes at least
one parameter.  One idea I had was to inherit from Proc, and define
#call, which would let flickr methods get passed around as, well,
methods, though this seems to have its own dangers.  (If I were to
implement this as a practical library, I think I'd use flickr's
reflection methods to sort this out, but is there a way to do it that
doesn't require that sort of external oracle?)

3. using method_missing strikes me as a potential pitfall, but I can
re-raise this if/when the flickr API returns its own 'method not
found' error.  Are there any other 'gotchas' I should watch out for?

Thanks in advance for any feedback.

matthew smillie.
Bc6d88907ce09158581fbb9b469a35a3?d=identicon&s=25 james_b (Guest)
on 2005-12-06 01:06
(Received via mailing list)
Matthew Smillie wrote:
>
> Hello all,
...

>
> In general I'm asking (like the subject suggests) if my  metaprogramming
> underpants are showing?  Have I defied any particular  conventions? Does
> this seem like a sensible approach, and if so, a  sensible
> implementation? Have I set myself up for some rather  spectacular failures?
>
> Specifically, though, there are three aspects of the code that I'm
> particularly curious whether anyone has any alternate approaches:


A suggestion: move the code that munges method_id and *params into
separate methods.  Much easier to test and make sure it does what you
want and that it doesn't barf on weird input.


James



--

http://www.ruby-doc.org       - Ruby Help & Documentation
http://www.artima.com/rubycs/ - Ruby Code & Style: Writers wanted
http://www.rubystuff.com      - The Ruby Store for Ruby Stuff
http://www.jamesbritt.com     - Playing with Better Toys
http://www.30secondrule.com   - Building Better Tools
31af45939fec7e3c4ed8a798c0bd9b1a?d=identicon&s=25 M.B.Smillie (Guest)
on 2005-12-06 03:20
(Received via mailing list)
On Dec 6, 2005, at 0:03, James Britt wrote:

>> I'm  particularly curious whether anyone has any alternate
>> approaches:
>
>
> A suggestion: move the code that munges method_id and *params into
> separate methods.  Much easier to test and make sure it does what
> you want and that it doesn't barf on weird input.

Done, with a due sense of embarrassment since I as soon as I started
thinking about that aspect of the code, I immediately found a bug
involving using capitalize/downcase and flickr's camelCase method
names.  Whoops & thanks for the reminder (I'll flatter myself that I
would have caught that during refactoring and testing anyway).

While I appreciate the input, I'm not particularly worried about
weird input and barfing at this stage, rather that my overall
approach with the metaprogramming is relatively sane.  I've barfed
over lots of weird input in my time (mixing wine and spirits, for
instance), but the metaprogramming is relatively new.

thanks again,
matthew smillie.
45196398e9685000d195ec626d477f0e?d=identicon&s=25 transfire (Guest)
on 2005-12-06 04:13
(Received via mailing list)
The whole class as method thing seems very odd.n I suspect there's a
better way. But I'm not sure what you're trying to do exactly (the link
to the Fliker API didn't work btw)

T.
31af45939fec7e3c4ed8a798c0bd9b1a?d=identicon&s=25 M.B.Smillie (Guest)
on 2005-12-06 04:54
(Received via mailing list)
----
Matthew Smillie            <M.B.Smillie@sms.ed.ac.uk>
Institute for Communicating and Collaborative Systems
University of Edinburgh


On Dec 6, 2005, at 2:37, Trans wrote:

> The whole class as method thing seems very odd.n I suspect there's a
> better way. But I'm not sure what you're trying to do exactly (the
> link
> to the Fliker API didn't work btw)
>
> T.

Gah.  Serves me right for doing this late at night.  Here is the
fixed link:

http://flickr.com/services/api/

Do you mind if I ask how you find it odd?  Or what you might do
otherwise?  Here is, hopefully, a fuller explanation:

The reasoning was like this.  I wanted the Ruby method calls to look
just like they're defined in the api (modulus the parameters), so
like this:
flickr.test.echo({"api_key" => "..."})

The catch is that in Ruby, that's a calling 'echo' on 'flickr.test',
and so the 'echo' there needs to somehow know what the name of the
entire method (flickr.test.echo) is.  The point of the exercise for
me was to avoid defining each and every API method specifically, so I
needed a way encode the entire API method name into that 'echo', and
the class heirarchy seemed like a reasonable fit:
  - simpler than tracing callers

So when a the above call is made, this happens:

  - flickr object create a Flickr::Test class, and an instance of it
in @test
  - flickr.test creates a Flickr::Test::Echo class, and an instance
of it in @echo, and then calls Flickr::Test::Echo#request
  - #request extracts the flickr API method name (flickr.test.echo)
by downcasing the first letter of each element in the class name (the
case-mangling was necessary since Ruby classnames are constants).

The first and simplest thing I did was to use method_missing just to
dynamically construct the API method name by concatenating the
method_id's in #method_missing and returning self.  This worked, but
didn't leave open much flexibility for adding, well, much of anything.

Another alternative I considered was instead of using instances of
the classes, was just to instead create a class method with analogous
behaviour (e.g. Flickr.test would create Flickr::Test, etc), but that
meant treating the Flickr class differently from the other classes,
since it would need to be instantiated to match the "flickr =
Flickr.new" intuition as well as the 'flickr.x.y" requirement.
Seemed better to do it all at once.

thanks once again,
matthew smillie.
45196398e9685000d195ec626d477f0e?d=identicon&s=25 transfire (Guest)
on 2005-12-06 05:50
(Received via mailing list)
> Here is, hopefully, a fuller explanation

Ah, I see. Okay. I'm too tired to go into tonight, but I get back to
you in the morning. In the mean time, iyou have some time, you might
want to have a look at the Functor class --that may give you some ideas
(see http://rubyforge.org/frs/?group_id=483)

T.
45196398e9685000d195ec626d477f0e?d=identicon&s=25 transfire (Guest)
on 2005-12-07 07:35
(Received via mailing list)
Sorry I didn't get to this until this evening. Hope it's helpful. -T.

  require 'calibre/functor'

  module Flickr
    extend self

    @@api = {}

    def method_missing( sym , *args )
        @@api[sym] ||= Functor.new { |op, *args|
            api_call("#{sym}.#{op}", *args )
        }
    end

    def api_call( method, *args )
      puts "call: http://flickr.com/services/rest/?method='#{method}...
      puts "with parameters:"
      p args
    end

  end

  Flickr.test.echo("api_key" => "something long", "foo" => "bar")
31af45939fec7e3c4ed8a798c0bd9b1a?d=identicon&s=25 M.B.Smillie (Guest)
on 2005-12-08 01:55
(Received via mailing list)
> Sorry I didn't get to this until this evening. Hope it's helpful. -T.

Noone's in a hurry over here, so no worries.


>   end
Well, it helps (and that calibre library is quite cool), but: it
fails when there's more than three terms in the method, e.g.:
Flickr.photos.licenses.getInfo({"something" => "blah"})
undefined method `getInfo' for nil:NilClass (NoMethodError)

The general case (flickr.a.b...n), doesn't seem possible to me
without the object returned from #method_missing having the same
behaviour (i.e. implementation of #method_missing) as the initial
object, but with the previous calls as part of the object's state.

matt.
45196398e9685000d195ec626d477f0e?d=identicon&s=25 transfire (Guest)
on 2005-12-08 04:09
(Received via mailing list)
Hmm.... that does make it trickier b/c when will the chain end? The
only thing I can think of the top of my head is to use an '!' method to
indicate it.

  Flickr.test.echo!

Then you can just return the same Functor-like object collecting the
parts along the way until the '!' is hit.

T.
This topic is locked and can not be replied to.