ineiti
ineiti

Reputation: 414

Lazy-evaluation of a "#{}"-string in ruby

I started to put print-statements throughout my code. So as not to clutter up the output, I did something like:

dputs LEVEL, "string"

where LEVEL is 0 for errors, 1 for important .. 5 for verbose and is compared to DEBUG_LEVEL. Now my problem is, that in a statement like:

dputs 5, "#{big_class.inspect}"

the string is always evaluated, also if I set DEBUG_LEVEL to 1. And this evaluation can take a long time. My favourite solution would be something like:

dputs 5, '#{big_class.inspect}'

and then evaluate the string if desired. But I don't manage to get the string in a form I can evaluate. So the only think I could come up with is:

dputs( 5 ){ "#{big_class.inspect}" }

but this looks just ugly. So how can I evaluate a '#{}' string?

Upvotes: 5

Views: 3085

Answers (4)

no-dashes
no-dashes

Reputation: 61

I think it's of no value whatsoever, but I just came up with:

2.3.1 :001 > s = '#{a}'
 => "\#{a}"
2.3.1 :002 > a = 1
 => 1
2.3.1 :003 > instance_eval s.inspect.gsub('\\', '')
 => "1"
2.3.1 :004 > s = 'Hello #{a} and #{a+1}!'
 => "Hello \#{a} and \#{a+1}!"
2.3.1 :005 > instance_eval s.inspect.gsub('\\', '')
 => "Hello 1 and 2!"

Don't use that in production :)

Upvotes: 2

ineiti
ineiti

Reputation: 414

OK, obviously I was just too lazy. I thought there must be a more clean way to do this, Ruby being the best programming language and all ;) To evaluate a string like

a = '#{1+1} some text #{big_class.inspect}'

only when needed, I didn't find a better way than going through the string and eval all "#{}" encountered:

str = ""
"#{b}\#{}".scan( /(.*?)(#\{[^\}]*\})/ ){
  str += $1
  str += eval( $2[2..-2] ).to_s
}

if you're not into clarity, you can get rid of the temporary-variable str:

"#{b}\#{}".scan( /(.*?)(#\{[^\}]*\})/ ).collect{|c|
  c[0] + eval( c[1][2..-2] ).to_s
}.join

The String.scan-method goes through every '#{}'-block, as there might be more than one, evaluating it (the 2..-2 cuts out the "#{" and "}") and putting it together with the rest of the string.

For the corner-case of the string not ending with a '#{}'-block, an empty block is added, just to be sure.

But well, after being some years in Ruby, this still feels clunky and C-ish. Perhaps it's time to learn a new language!

Upvotes: 0

Andrew Marshall
Andrew Marshall

Reputation: 96914

You could do this by having dputs use sprintf (via %). That way it can decide not to build the interpolated string unless it knows it's going to print it:

def dputs(level, format_str, *vars)
  puts(format_str % vars) if level <= LEVEL
end

LEVEL = 5
name = 'Andrew'
dputs 5, 'hello %s', name
#=> hello Andrew

Or, as you suggest, you can pass a block which would defer the interpolation till the block actually runs:

def dputs(level, &string)
  raise ArgumentError.new('block required') unless block_given?
  puts string.call if level <= LEVEL
end

Upvotes: 6

Mike Larsen
Mike Larsen

Reputation: 346

I don't think you can dodge the ugly there. The interpolation happens before the call to dputs unless you put it inside a block, which postpones it until dputs evaluates it. I don't know where dputs comes from, so I'm not sure what its semantics are, but my guess is the block would get you the lazy evaluation you want. Not pretty, but it does the job.

Upvotes: 1

Related Questions