Are my metaprogramming underpants showing?

  • Thread starter Matthew Smillie
  • Start date
M

Matthew Smillie

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.
 
J

James Britt

Matthew said:
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
 
M

Matthew Smillie

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.
 
T

Trans

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.
 
M

Matthew Smillie

----
Matthew Smillie <[email protected]>
Institute for Communicating and Collaborative Systems
University of Edinburgh


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.
 
T

Trans

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.
 
T

Trans

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")
 
M

Matthew Smillie

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.

module Flickr
extend self

@@api = {}

def method_missing( sym , *args )
@@api[sym] ||= Functor.new { |op, *args|
api_call("#{sym}.#{op}", *args )
}
end
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.
 
T

Trans

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.
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,781
Messages
2,569,615
Members
45,296
Latest member
HeikeHolli

Latest Threads

Top