开发者

How to do "late" string interpolation in Ruby

>> string = '#开发者_JAVA技巧{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.


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!"


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.


It works with eval:

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

(If you trust the value of string.)


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


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.


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.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜