>> string = \'#{var}\'
=> \"\\#{var}\"
>> proc = Proc.new { |var| string }
=> #
>> proc.call(123)
=> \"\\
To clarify, those looking for the answer to this question's title (how to do late string interpolation) should understand that there is no such thing as late string interpolation.
The poster of this question didn't actually care about string interpolation, per se, just formatting. For those that also don't care about the distinction between string interpolation and string formatting in Ruby, many of the answers here are fine.
If you don't know whether or not you care, there are plenty of responses to stackoverflow questions that explain the nuanced differences -- here's one more explanation:
In general, string interpolation is compile-time syntactic sugar: the format string is "compiled" exactly once (when generating byte code). With string formatting, the format string is re-parsed on every invocation (see Kernel::sprintf).
To see this, consider this program:
x=1
s="#{x}"
The RubyVM compiles it to bytecode that includes a hard reference to x
(see instruction 0005):
$ ruby --dump=insns <@-:1 (1,0)-(2,8)> (catch: FALSE)
local table (size: 2, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])
[ 2] x@0 [ 1] s@1
0000 putobject_INT2FIX_1_ ( 1)[Li]
0001 setlocal_WC_0 x@0
0003 putobject "" ( 2)[Li]
0005 getlocal_WC_0 x@0
0007 dup
0008 checktype T_STRING
0010 branchif 17
0012 dup
0013 opt_send_without_block ,
0016 tostring
0017 concatstrings 2
0019 dup
0020 setlocal_WC_0 s@1
0022 leave
Note that the interpolated string does not appear, as it has been compiled to one call to x.to_s
(instruction 0013) and one to the concatstrings
VM instruction (instruction 0017).
On the other hand, a solution like:
x=1
s="%{foo}" % {foo: x}
Will, on every invocation, push a format string onto the stack (instruction 0003) and invoke String#% (instruction 0007) to re-parse it, essentially triggering the same format-parsing code described in the Kernel::sprintf documentation.
$ ruby --dump=insns < x=1
> s="%{foo}" % {foo: x}
> EXAMPLE
== disasm: #@-:1 (1,0)-(2,21)> (catch: FALSE)
local table (size: 2, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])
[ 2] x@0 [ 1] s@1
0000 putobject_INT2FIX_1_ ( 1)[Li]
0001 setlocal_WC_0 x@0
0003 putstring "%{foo}" ( 2)[Li]
0005 getlocal_WC_0 x@0
0007 opt_send_without_block ,
0010 dup
0011 setlocal_WC_0 s@1
0013 leave
Most use cases won't care about this distinction, but if you came to this question because you do, there you go.