Problems with arity when converting a method to a block/proc that accepts an array param

Posted by Tim Connor Sat, 17 Jan 2009 04:55:00 GMT

Some consider this hacky, but while working on cleaning up test_benchmark I wanted to pass a named method as a block to a map. In the simplest case you just grab a method reference via object.method(:method_name) and to_proc it with an ampersand. In this case the method definition is basically just a way to store a proc, and this could be replaced by just storing a proc in an instance variable. But it works, both with the method called directly, and passed as a proc/block:

benchmarks = [["a slow test",2.0],["a fast test",0.001]]
def by_time(a,b)
  b[1] <=> a[1]
end
benchmarks.sort { |a,b| by_time(a,b) } #works
benchmarks.sort(&method(:by_time)) #works

I discovered you can have problems with arity, though, if the block method is passed an array, which is then expanded out to array.length params, giving you an ArgumentError. For instance, if I am again working with doubles for my benchmark rows:

benchmarks = [["a slow test",2.0],["a fast test",0.001]]
def format_row(tuple)
  ("%0.3f" % tuple[1]) + " #{tuple[0]}"
end
benchmarks.map { |tuple| format_row(tuple } #works
benchmarks.map(&method(:format_row))
# ArgumentError: wrong number of arguments (2 for 1)

Thanks to Evan Phoenix for pointing out I just need to use a glob param in my method. This won’t work with the direct call anymore, as the tuple gets wrapped in another array by the globbing:

benchmarks = [["a slow test",2.0],["a fast test",0.001]]
def format_row(*tuple)
  ("%0.3f" % tuple[1]) + " #{tuple[0]}"
end
benchmarks.map(&method(:format_row)) #works
benchmarks.map { |tuple| format_row(tuple }
#Blows up on an invalid index access, because tuple inside format_row is now [["a slow test",2.0]] for the first call

To work around this, in this case, it works to just flatten the array.

def format_row(*tuple)
  tuple.flatten!
  ("%0.3f" % tuple[1]) + " #{tuple[0]}"
end
benchmarks.map(&method(:format_row)) #works
benchmarks.map { |tuple| format_row(tuple } #works

I guess the general conclusion to go with, is be careful doing tricky things like using method references as procs. Especially keep a careful eye on the method signature and how arrays can mess with your arity.

Determining where in the inheritance chain a method is defined

Posted by Tim Connor Tue, 16 Dec 2008 19:19:00 GMT

Define in your .irbrc.

def Object.method_defined_where(method)
  self.ancestors.detect { |a| a.methods(false).include?(method.to_s) }
end
 
#Usage
#File.method_defined_where('read')
#=> IO

jbarnette and bougyman in #caboose helped me get it closer to actually working. It still probably needs some tuning. Feel free to fork and tweak the gist on method_defined_where, and then comment here.

Man, I wish I had known about this snippet earlier 1

Posted by Tim Connor Thu, 10 Apr 2008 08:57:00 GMT

as it would have made plenty of tooling around exploring (and even a job interview) easier. It’s much nicer than just sorting the whole mess after you decide it would take less time to find what you want then implement this very functionality.

# List instance methods without ancestor methods
String.instance_methods(false)

From 5 Ways to Sharpen Your Ruby-foo

Calling (invoking) rails rake tasks from within ruby, for testing, try 2 3

Posted by Tim Connor Thu, 21 Feb 2008 21:48:00 GMT

Yesterday I wrote a post on capturing the output of rake from a ruby call using backticks, because I wanted to do so in a test. Well, I had Date.today stubbed out, but got lazy and used yesterday’s date, so of course when I come in today the test fails. In the light of a new day my problem was obvious. If you call rake from a sub-shell, say via ``, of course your mocking will not exist in that process. So today, I had Another Wonderful Opportunity for Learning (there are acronyms for this that use a different word than wonderful): a chance to do it right.

It took a little digging to figure out how to get this to work right with the built-in rails tasks, so here you go:


require ‘rake’
require ‘rake/rdoctask’
require ‘rake/testtask’
require ‘tasks/rails’

def capture_stdout
s = StringIO.new
oldstdout = $stdout
$stdout = s
yield
s.string
ensure
$stdout = oldstdout
end

Rake.application.rake_require ‘../../lib/tasks/metric_fetcher’
results = capture_stdout {Rake.application[‘metric_fetcher’].invoke}

I liberated capture_stdout from the rake tests themselves. In retrospect, I should have just looked there first. As TDD and especially BDD get even more widespread the tests are often the easiest place to look for good examples of usage.

Capturing a system call in ruby with backticks, while setting an environment variable 1

Posted by Tim Connor Wed, 20 Feb 2008 18:26:00 GMT

As you may know system() and `` both make system calls in ruby, but the second one captures the STDOUT for you, essentially, which is handy sometimes, like when testing. They don’t necessarily both do this by just a straight call to the commandline, but various internal utilities that make such thing possible in Windows too, for instance.

Well the problem with this is it can mess with your setting of an ENV as a precursor to your command. For instance, this is valid on my platform:


RAILS_ENV=test rake metric_fetcher

and works via system():

system(“RAILS_ENV=test rake metric_fetcher”)

but chokes in backticks, probably due to the aforementioned jiggering around for Windows compat:

`RAILS_ENV=test rake metric_fetcher`

Now you generally can just move the command back to the beginning of the line:

`rake RAILS_ENV=test metric_fetcher`

but if for some bizarre reason that doesn’t work for you (it’s probably something else going, wrong, though, to be honest) you can do this:

ENV[‘RAILS_ENV’]=‘test’; `rake metric_fetcher`

So ultimately, I can do this hacky wonder:


assert `rake RAILS_ENV=test metric_fetcher`.empty?

if my rake task succeeds silently and fails noisily (which is a useful characteristic for cron’ed tasks).

Thanks to Defiler and ocotpod for proving to me I really did have my head up my ass and that all but the leading env var in backticks form should work as expected.(fixture set-up issues as usual).

Addendum And if you are calling rake like this in a test and getting odd problems – consider turning transaction fixtures off.

utility_belt tweak 2 - passing a string directly to pastie

Posted by Tim Connor Wed, 12 Dec 2007 00:57:00 GMT

Instead of having to run it through the clipboard first:


def pastie(string=nil)
pastie_url = Net::HTTP.post_form(URI.parse(“http://pastie.caboo.se/pastes/create”),
{"paste_parser" => “ruby”,
“paste[authorization]” => “burger”,
“paste[body]” => (string || MacClipboard.read)}).body.match(/href=“([^\”]+)“/)1
MacClipboard.write(pastie_url)
system(”open #{pastie_url}")
pastie_url
end

If you are wondering WTF is going on, read the previous post

Textmateing a string from irb - a tweak for Gile's new gem, utility_belt

Posted by Tim Connor Wed, 12 Dec 2007 00:37:00 GMT

So Giles Bowkett just released a gem that does most of the tweaks I had been meaning to add to my .irbrc, but had been to lazy. utility_belt is pretty awesome, but there is one tiny tweak I wanted to add to make it optimal, and Giles isn’t known for making himself easy to get a hold of (especially now that comments seem to be perpetually closed on his blog). So instead I just hacked it on the gem myself. I’m not going to bother distributing it as a gem myself, but you are welcome to steal the idea and add it to your copy.

To be able to pass a string (say like the last result, via “_”, when irb just dumped 500 lines of output on you) to Textmate, edit lib/interactive_editor.rb like so


10,13c10,13
< def edit
< unless @file
< @file = Tempfile.new(“irb_tempfile”)
< end
-
> def edit(string)
> @file = Tempfile.new(“irb_tempfile”)
> @file.write string
> @file.close
17a18
> @file.unlink
22c23
< def edit(editor)
-
> def edit(editor, string)
27c28
< IRB.conf[:interactive_editors][editor].edit
-
> IRB.conf[:interactive_editors][editor].edit(string)
30,31c31,32
< def vi
< edit(:vim)
-
> def vi(string=nil)
> edit(:vim,string)
34,35c35,36
< def mate
< edit(:mate)
-
> def mate(string=nil)
> edit(:mate,string)
38,39c39,40
< def emacs
< edit(:emacs)
-
> def emacs(string=nil)
> edit(:emacs,string)

Now glory in “mate _” from irb.

Or best of all: use ruby2ruby and call mate on the result of #to_ruby on a method. You can edit the method in Textmate and save, and wallah, it’s applied. Of course, getting that working correctly in an actual Module/Class hierarchy might be a bit more complicated, and I leave as an exercise for the reader.

Using define_method to overwrite a class' initialize as an example of overwriting a method from your mock, version 2 1

Posted by Tim Connor Fri, 30 Nov 2007 22:40:00 GMT

Last year, I wrote an article about redefining an instance method from a module via alias_method. I understand better how extend/include work together now (and the ClassMethods include patterns and whatnot) so I am little embarrassed at my earlier confusion, but there is one concrete thing I wanted to add/simplify.

If you are mocking and don’t need to keep a reference to the old method, using define_method might be marginally cleaner.

def self.included(associator_class) 
  associator_class.send(:define_method, :initialize, instance_method(:initialize))
end

This is handier because you can skip the method definition/reference logic and just do it in situ, for instant-stubification


ClassToBeIntercepted.send :define_method, :method_to_be_nuked, proc {return ’nuked’}

Principle of MOST surprise - aka ruby hazing 101

Posted by Tim Connor Thu, 16 Nov 2006 02:18:00 GMT

I suspect that the ruby Illuminati are going to have me killed for revealing this, because it seems to be a little booby-trap of goodness that is part of the learning experience, but WTF?!

irb(main):001:0> nil.id == 4
(irb):2: warning: Object#id will be deprecated; use Object#object_id
=> true

What this means, of course, is don’t be sloppy and assign from object.id when object might be nil, with no check, or you’ll be in for a surprise. For example, you could end up assigning an object to an admin user, when it should have no user assigned.