29
>> string = '#{var}'
=> "\#{var}"

>> proc = Proc.new { |var| string }
=> #<Proc:0xb717a8c4@(pry):6>

>> proc.call(123)
=> "\#{var}"

Not really what I want. Double quotes around string result in the obvious undefined local variable.

Paweł Gościcki
  • 9,066
  • 5
  • 70
  • 81

6 Answers6

33

In my case I needed to have configuration stored inside a yml, with interpolation, but which is only interpolated when I need it. The accepted answer with the Proc seemed overly complicated to me.

In ruby 1.8.7 you can use the % syntax as follows:

"This is a %s verb, %s" % ["nice", "woaaaah"]

When using at least ruby 1.9.x (or ruby 1.8.7 with i18n) there is a cleaner alternative:

my_template = "This is a %{adjective} verb, %{super}!"

my_template % { adjective: "nice", super: "woah" }
=> "This is a nice verb, woah!"
nathanvda
  • 49,707
  • 13
  • 117
  • 139
  • I was looking for something like a nice safe erb replacement for a very minor use case. This really helped! – Daniel Dec 22 '21 at 18:19
21

Although this is possible, it's not going to work how you intend here without having to use eval, and generally that's a bad idea if there's an alternative. The good news is you have several options.

The most straightforward is to use sprintf formatting which is made even easier with the String#% method:

string = '%s'

proc = Proc.new { |var| string % var }

proc.call(123)
# => "123"

This is a really reliable method as anything that supports the .to_s method will work and won't cause the universe to implode if it contains executable code.

tadman
  • 208,517
  • 23
  • 234
  • 262
  • 11
    `String#%` also supports named parameters with Hash as an argument, which brings it really close to the power of native string interpolation. – Mladen Jablanović Sep 12 '11 at 19:22
  • I generally use it C-style for reasons of being compact that way, but as you point out it does a lot more than that. – tadman Sep 12 '11 at 19:44
5

It works with eval:

proc = Proc.new { |var| eval(%Q{"#{string}"}) }

(If you trust the value of string.)

Arnaud Le Blanc
  • 98,321
  • 23
  • 206
  • 194
  • 1
    When you're quoting quotes, you should probably express it using singles or `%q[]` like: `%q["#{string}"]` to avoid all the awful escaping. – tadman Sep 12 '11 at 19:00
  • Hmm... seems like `eval` works. But maybe there is another way? – Paweł Gościcki Sep 12 '11 at 19:01
  • @Paweł, any late-interpolation with ruby's #{} syntax would do evals anyway, as you can put any ruby expression in #{}, that would need to be evaluated – Arnaud Le Blanc Sep 12 '11 at 19:06
  • I don't know why you don't just `eval(string)` if that's what you want. Scary, dangerous. I've seen this used in the custom finder SQL for ActiveRecord and it always struck me as a very bad example. There's nothing to say you can't `gsub` the pattern for `%s` substitution and interpolate with that, roughly speaking, instead of having to go with the nuclear option. – tadman Sep 12 '11 at 19:16
  • I agree, but this was not the question :) – Arnaud Le Blanc Sep 12 '11 at 19:24
  • For gsub, only if you support only variable interpolation, not *any expression* interpolation. – Arnaud Le Blanc Sep 12 '11 at 19:26
  • 4
    But `%Q` will do interpolation so you could `%Q{#{string}}` if you were so inclined. – mu is too short Sep 12 '11 at 20:13
4

You can achieve the DRY that you're seeking by creating a "curried" function (ie: a Proc that returns a Proc) where the inner function contains the base string with variables for every part that differs.

Correct me if I'm wrong, but in your code behind your commented link, the only difference between the two strings is a single char at the end. (Even if it isn't, you can still use this technique to achieve the same goal.) You can create a Proc that returns a Proc that contains your string, then call the outer Proc twice for your two trailing characters:

rails_root = "whatever" # Not variant for the string
rails_env_prompt = "whatever" #not variant for the string

spec = Proc.new { |tail_char| 
  Proc.new {|obj, nest_level, *| 
    "#{rails_root} #{rails_env_prompt} #{obj}:#{nest_level}#{tail_char} "
  }
}

Pry.config.prompt = [ spec.call(">"), spec.call("*") ]  

Pry.config.prompt[0].call("My obj", "My Nest Level")
# result: "whatever whatever My obj:My Nest Level> "
Craig Walker
  • 49,871
  • 54
  • 152
  • 212
  • That's way to complicated :) String interpolation from the accepted answers works for me. – Paweł Gościcki Apr 03 '13 at 08:30
  • 1
    IMHO, for the benefit of people who come to this question because of the title, *this* should be the accepted answer. Neither the currently accepted answer (by nathanvda) nor the currently most-upvoted-answer (by Arnaud Le Blanc) actually use string interpolation (a compile-time mechanism). Instead, they suggest string-formatting (a runtime mechanism). For those who care about the difference between them, the currying technique here is a great solution. (If one doesn't care, that's fine, but that's not what the title of this question asks.) – Mickalot Feb 17 '20 at 18:31
1

Do you have to define the interpolation-bearing string outside of your Proc?

proc = Proc.new { |var| "#{var}" }
proc.call(123) # "123"

This would be the cleanest way I think.

Craig Walker
  • 49,871
  • 54
  • 152
  • 212
  • I see that this doesn't work for your particular example... but it still might apply to other similar situations. I'll post a better answer separately. – Craig Walker Apr 02 '13 at 18:12
1

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 <<EXAMPLE
x=1
s="#{x}"
EXAMPLE
== disasm: #<ISeq:<main>@-: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       <callinfo!mid:to_s, argc:0, FCALL|ARGS_SIMPLE>, <callcache>
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 <<EXAMPLE
> x=1
> s="%{foo}" % {foo: x}
> EXAMPLE
== disasm: #<ISeq:<main>@-: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       <callinfo!mid:%, argc:1, kw:[foo], KWARG>, <callcache>
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.

Mickalot
  • 2,431
  • 3
  • 22
  • 23