String Concatenation Vs. Interpolation in Ruby

String concatenation vs. interpolation in Ruby

Whenever TIMTOWTDI (there is more than one way to do it), you should look for the pros and cons. Using "string interpolation" (the second) instead of "string concatenation" (the first):

Pros:

  • Is less typing
  • Automatically calls to_s for you
  • More idiomatic within the Ruby community
  • Faster to accomplish during runtime

Cons:

  • Automatically calls to_s for you (maybe you thought you had a string, and the to_s representation is not what you wanted, and hides the fact that it wasn't a string)
  • Requires you to use " to delimit your string instead of ' (perhaps you have a habit of using ', or you previously typed a string using that and only later needed to use string interpolation)

What's the difference between these six Ruby string syntaxes, are they concatenation, interpolation or maybe something else

This is a NoMethodError

style_i_used = 'a format to' /
'span multiple lines'

/ is a method call which Strings don't have. \ on the other hand "escapes" the newline so it's treated as one string.

These won't give an error, but are bad:

my_original = 'what I originally had' +
'across multiple lines'

s_one = 'I assume this is the same as '
s_two = 'the first example with the '
s_three = 'code being more verbose'
my_string = s_one + s_two + s_three

my_first_solution = 'that breaks whenever'
my_first_solution << 'ruby 3.0 might be released'

+ and << are method calls. I don't think the Ruby parser is smart enough to see that these can all be compile-time strings so these strings are actually being constructed during run-time with object allocation and everything. That is entirely unnecessary.

The other kinds have different usage/results, so you use whichever is appropriate.

str = "a"\
"b" # result is "ab"

str = "a\
b" # also "ab"

str = "a
b" # result is "a\nb"

str = <<END
'"a'"
'"b'"
END
# result is " '\"a'\"\n '\"b'\""

str = <<~END
'"a'"
'"b'"
END
# result is "'\"a'\"\n'\"b'\""

String concatenation in Ruby

You can do that in several ways:

  1. As you shown with << but that is not the usual way
  2. With string interpolation

    source = "#{ROOT_DIR}/#{project}/App.config"
  3. with +

    source = "#{ROOT_DIR}/" + project + "/App.config"

The second method seems to be more efficient in term of memory/speed from what I've seen (not measured though). All three methods will throw an uninitialized constant error when ROOT_DIR is nil.

When dealing with pathnames, you may want to use File.join to avoid messing up with pathname separator.

In the end, it is a matter of taste.

Ruby - Array.join versus String Concatenation (Efficiency)

Try it yourself with the Benchmark class.

require "benchmark"

n = 1000000
Benchmark.bmbm do |x|
x.report("concatenation") do
foo = ""
n.times do
foo << "foobar"
end
end

x.report("using lists") do
foo = []
n.times do
foo << "foobar"
end
string = foo.join
end
end

This produces the following output:

Rehearsal -------------------------------------------------
concatenation 0.300000 0.010000 0.310000 ( 0.317457)
using lists 0.380000 0.050000 0.430000 ( 0.442691)
---------------------------------------- total: 0.740000sec

user system total real
concatenation 0.260000 0.010000 0.270000 ( 0.309520)
using lists 0.310000 0.020000 0.330000 ( 0.363102)

So it looks like concatenation is a little faster in this case. Benchmark on your system for your use-case.

ruby: workarounds for nested string interpolation

Yes, a variable will make this more readable (imo):

prefix = "#{x} is " if x
"#{prefix}ok"

(this relies on the fact that nil#to_s == '')

Kotlin - String interpolation $ vs concatenate using plus-equals +=

The best way to find the difference is to look at the generated bytecode (I will use Kotlin 1.4.10).

Bytecode generated for name += " $someInt":

ALOAD 0
NEW java/lang/StringBuilder
DUP
INVOKESPECIAL java/lang/StringBuilder.<init> ()V
SWAP
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
BIPUSH 32
INVOKEVIRTUAL java/lang/StringBuilder.append (C)Ljava/lang/StringBuilder;
ILOAD 1
INVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder;
INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
ASTORE 0

Bytecode generated for name = "$name $someInt":

NEW java/lang/StringBuilder
DUP
INVOKESPECIAL java/lang/StringBuilder.<init> ()V
ALOAD 0
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
BIPUSH 32
INVOKEVIRTUAL java/lang/StringBuilder.append (C)Ljava/lang/StringBuilder;
ILOAD 1
INVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder;
INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
ASTORE 0

Results are pretty much the same, but in the first case there is an extra SWAP operation, cause argument for first append operation was loaded onto stack too early (before StringBuilder was created, and now they need to be swapped).

TL;DR

Turns out that:

  1. StringBuilder is created in both cases
  2. There are 3 appends (name, whitespace & someInt) in both cases
  3. Second one is slightly more effective.

Difference between String interpolation and String concatenation

From a speed point of view, to differentiate concatenation (value1 + "-" + value2) and interpolation ("\(value1)-\(value2)"), results may depend on the number of operations done to obtain the final string.

My results on an iPhone 8 show that:

  • if there is roughly < 30 substrings to attach together, then concatenation is faster
  • if there is roughly > 30 substrings to attach together, then interpolation is faster

Thank you Sirens for figuring out that one wasn't always faster than the other!

Try it yourself (and don't forget to adapt the tested character set and iterations for your needs):

import UIKit

class ViewController: UIViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)

DispatchQueue.global(qos: .default).async {
ViewController.buildDataAndTest()
}
}

private static func buildDataAndTest(times: Int = 1_000) {
let characterSet = CharacterSet.alphanumerics
characterSet.cacheAllCharacters()
let data: [(String, String)] = (0 ..< times).map { _ in
(characterSet.randomString(length: 50), characterSet.randomString(length: 20))
}
_ = testCIA(data)
_ = testInterpol(data)
print("concatenation: " + String(resultConcatenation))
print("interpolation: \(resultInterpolation)")
}

/// concatenation in array
static var resultConcatenation: CFTimeInterval = 0
private static func testCIA(_ array: [(String, String)]) -> String {
var foo = ""
let start = CACurrentMediaTime()
for (a, b) in array {
foo = foo + " " + a + "+" + b
}
resultConcatenation = CACurrentMediaTime() - start
return foo
}

/// interpolation
static var resultInterpolation: CFTimeInterval = 0
private static func testInterpol(_ array: [(String, String)]) -> String {
var foo = ""
let start = CACurrentMediaTime()
for (a, b) in array {
foo = "\(foo) \(a)+\(b)"
}
resultInterpolation = CACurrentMediaTime() - start
return foo
}
}

extension CharacterSet {
static var cachedCharacters: [Character] = []

public func cacheAllCharacters() {
CharacterSet.cachedCharacters = characters()
}

/// extracting characters
/// https://stackoverflow.com/a/52133647/1033581
public func characters() -> [Character] {
return codePoints().compactMap { UnicodeScalar($0) }.map { Character($0) }
}
public func codePoints() -> [Int] {
var result: [Int] = []
var plane = 0
for (i, w) in bitmapRepresentation.enumerated() {
let k = i % 8193
if k == 8192 {
plane = Int(w) << 13
continue
}
let base = (plane + k) << 3
for j in 0 ..< 8 where w & 1 << j != 0 {
result.append(base + j)
}
}
return result
}

// http://stackoverflow.com/a/42895178/1033581
public func randomString(length: Int) -> String {
let charArray = CharacterSet.cachedCharacters
let charArrayCount = UInt32(charArray.count)
var randomString = ""
for _ in 0 ..< length {
randomString += String(charArray[Int(arc4random_uniform(charArrayCount))])
}
return randomString
}
}

Concatenating string with number in ruby

This isn't exactly concatenation but it will do the job you want to do:

puts " Total Revenue of East Cost: #{total_revenue_of_east_cost}"

Technically, this is interpolation. The difference is that concatenation adds to the end of a string, where as interpolation evaluates a bit of code and inserts it into the string. In this case, the insertion comes at the end of your string.

Ruby will evaluate anything between braces in a string where the opening brace is preceded by an octothorpe.

Surprising string concatenation

This only works for string literals, and a part of the literal syntax.

If you have 2 string literals with just whitespace between them, they get turned into a single string. It's a convention borrowed from later versions of C.

string concatenation and yielding block based on if modifier

You can't concatenate strings like that. The line yield(block) + isn't a complete line. That's why you're getting the errors. Here are two possible fixes:

def wrapper(form, attr, options = {}, &block)
if block_given?
return yield(block) + form.label(form_label, class: "control-label")
end
form.label(form_label, class: "control-label")
end

Or this

def wrapper(form, attr, options = {}, &block)
content = ''
if block_given?
content = yield(block)
end
content + form.label(form_label, class: "control-label")
end


Related Topics



Leave a reply



Submit