|
| 1 | +--- |
| 2 | +layout: singlepage-overview |
| 3 | +title: String Interpolation |
| 4 | +partof: string-interpolation |
| 5 | + |
| 6 | +languages: [es, ja, zh-cn] |
| 7 | + |
| 8 | +permalink: /overviews/core/:title.html |
| 9 | +--- |
| 10 | + |
| 11 | +**Josh Suereth** |
| 12 | + |
| 13 | +## Introduction |
| 14 | + |
| 15 | +Starting in Scala 2.10.0, Scala offers a new mechanism to create strings from your data: String Interpolation. |
| 16 | +String Interpolation allows users to embed variable references directly in *processed* string literals. Here's an example: |
| 17 | + |
| 18 | +{% tabs example-1 %} |
| 19 | +{% tab 'Scala 2 and 3' for=example-1 %} |
| 20 | + val name = "James" |
| 21 | + println(s"Hello, $name") // Hello, James |
| 22 | +{% endtab %} |
| 23 | +{% endtabs %} |
| 24 | + |
| 25 | +In the above, the literal `s"Hello, $name"` is a *processed* string literal. This means that the compiler does some additional |
| 26 | +work to this literal. A processed string literal is denoted by a set of characters preceding the `"`. String interpolation |
| 27 | +was introduced by [SIP-11](https://docs.scala-lang.org/sips/pending/string-interpolation.html), which contains all details of the implementation. |
| 28 | + |
| 29 | +## Usage |
| 30 | + |
| 31 | +Scala provides three string interpolation methods out of the box: `s`, `f` and `raw`. |
| 32 | + |
| 33 | +### The `s` String Interpolator |
| 34 | + |
| 35 | +Prepending `s` to any string literal allows the usage of variables directly in the string. You've already seen an example here: |
| 36 | + |
| 37 | +{% tabs example-2 %} |
| 38 | +{% tab 'Scala 2 and 3' for=example-2 %} |
| 39 | + val name = "James" |
| 40 | + println(s"Hello, $name") // Hello, James |
| 41 | +{% endtab %} |
| 42 | +{% endtabs %} |
| 43 | + |
| 44 | +Here `$name` is nested inside an `s` processed string. The `s` interpolator knows to insert the value of the `name` variable at this location |
| 45 | +in the string, resulting in the string `Hello, James`. With the `s` interpolator, any name that is in scope can be used within a string. |
| 46 | + |
| 47 | +String interpolators can also take arbitrary expressions. For example: |
| 48 | + |
| 49 | +{% tabs example-3 %} |
| 50 | +{% tab 'Scala 2 and 3' for=example-3 %} |
| 51 | + println(s"1 + 1 = ${1 + 1}") |
| 52 | +{% endtab %} |
| 53 | +{% endtabs %} |
| 54 | + |
| 55 | +will print the string `1 + 1 = 2`. Any arbitrary expression can be embedded in `${}`. |
| 56 | + |
| 57 | +For some special characters, it is necessary to escape them when embedded within a string. |
| 58 | +To represent an actual dollar sign you can double it `$$`, like here: |
| 59 | + |
| 60 | +{% tabs example-4 %} |
| 61 | +{% tab 'Scala 2 and 3' for=example-4 %} |
| 62 | + println(s"New offers starting at $$14.99") |
| 63 | +{% endtab %} |
| 64 | +{% endtabs %} |
| 65 | + |
| 66 | +which will print the string `New offers starting at $14.99`. |
| 67 | + |
| 68 | +Double quotes also need to be escaped. This can be done by using triple quotes as shown: |
| 69 | + |
| 70 | +{% tabs example-5 %} |
| 71 | +{% tab 'Scala 2 and 3' for=example-5 %} |
| 72 | + val person = """{"name":"James"}""" |
| 73 | +{% endtab %} |
| 74 | +{% endtabs %} |
| 75 | + |
| 76 | +which will produce the string `{"name":"James"}` when printed. |
| 77 | + |
| 78 | +### The `f` Interpolator |
| 79 | + |
| 80 | +Prepending `f` to any string literal allows the creation of simple formatted strings, similar to `printf` in other languages. When using the `f` |
| 81 | +interpolator, all variable references should be followed by a `printf`-style format string, like `%d`. Let's look at an example: |
| 82 | + |
| 83 | +{% tabs example-6 %} |
| 84 | +{% tab 'Scala 2 and 3' for=example-6 %} |
| 85 | + val height = 1.9d |
| 86 | + val name = "James" |
| 87 | + println(f"$name%s is $height%2.2f meters tall") // James is 1.90 meters tall |
| 88 | +{% endtab %} |
| 89 | +{% endtabs %} |
| 90 | + |
| 91 | +The `f` interpolator is typesafe. If you try to pass a format string that only works for integers but pass a double, the compiler will issue an |
| 92 | +error. For example: |
| 93 | + |
| 94 | +{% tabs f-interpolator-error class=tabs-scala-version %} |
| 95 | + |
| 96 | +{% tab 'Scala 2' for=f-interpolator-error %} |
| 97 | +```scala |
| 98 | +val height: Double = 1.9d |
| 99 | + |
| 100 | +scala> f"$height%4d" |
| 101 | +<console>:9: error: type mismatch; |
| 102 | + found : Double |
| 103 | + required: Int |
| 104 | + f"$height%4d" |
| 105 | + ^ |
| 106 | +``` |
| 107 | +{% endtab %} |
| 108 | + |
| 109 | +{% tab 'Scala 3' for=f-interpolator-error %} |
| 110 | +```scala |
| 111 | +val height: Double = 1.9d |
| 112 | + |
| 113 | +scala> f"$height%4d" |
| 114 | +-- Error: ---------------------------------------------------------------------- |
| 115 | +1 |f"$height%4d" |
| 116 | + | ^^^^^^ |
| 117 | + | Found: (height : Double), Required: Int, Long, Byte, Short, BigInt |
| 118 | +1 error found |
| 119 | + |
| 120 | +``` |
| 121 | +{% endtab %} |
| 122 | + |
| 123 | +{% endtabs %} |
| 124 | + |
| 125 | +The `f` interpolator makes use of the string format utilities available from Java. The formats allowed after the `%` character are outlined in the |
| 126 | +[Formatter javadoc](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/Formatter.html#detail). If there is no `%` character after a variable |
| 127 | +definition a formatter of `%s` (`String`) is assumed. |
| 128 | + |
| 129 | +### The `raw` Interpolator |
| 130 | + |
| 131 | +The raw interpolator is similar to the `s` interpolator except that it performs no escaping of literals within the string. Here's an example processed string: |
| 132 | + |
| 133 | +{% tabs example-7 %} |
| 134 | +{% tab 'Scala 2 and 3' for=example-7 %} |
| 135 | + scala> s"a\nb" |
| 136 | + res0: String = |
| 137 | + a |
| 138 | + b |
| 139 | +{% endtab %} |
| 140 | +{% endtabs %} |
| 141 | + |
| 142 | +Here the `s` string interpolator replaced the characters `\n` with a return character. The `raw` interpolator will not do that. |
| 143 | + |
| 144 | +{% tabs example-8 %} |
| 145 | +{% tab 'Scala 2 and 3' for=example-8 %} |
| 146 | + scala> raw"a\nb" |
| 147 | + res1: String = a\nb |
| 148 | +{% endtab %} |
| 149 | +{% endtabs %} |
| 150 | + |
| 151 | +The raw interpolator is useful when you want to avoid having expressions like `\n` turn into a return character. |
| 152 | + |
| 153 | +In addition to the three default string interpolators, users can define their own. |
| 154 | + |
| 155 | +## Advanced Usage |
| 156 | + |
| 157 | +In Scala, all processed string literals are simple code transformations. Anytime the compiler encounters a string literal of the form: |
| 158 | + |
| 159 | +{% tabs example-9 %} |
| 160 | +{% tab 'Scala 2 and 3' for=example-9 %} |
| 161 | + id"string content" |
| 162 | +{% endtab %} |
| 163 | +{% endtabs %} |
| 164 | + |
| 165 | +it transforms it into a method call (`id`) on an instance of [StringContext](https://www.scala-lang.org/api/current/scala/StringContext.html). |
| 166 | +This method can also be available on implicit scope. |
| 167 | +To define our own string interpolation, we need to create an implicit class (Scala 2) or an `extension` method (Scala 3) that adds a new method to `StringContext`. |
| 168 | +Here's an example: |
| 169 | + |
| 170 | +{% tabs json-definition-and-usage class=tabs-scala-version %} |
| 171 | + |
| 172 | +{% tab 'Scala 2' for=json-definition-and-usage %} |
| 173 | +```scala |
| 174 | +// Note: We extends AnyVal to prevent runtime instantiation. See |
| 175 | +// value class guide for more info. |
| 176 | +implicit class JsonHelper(val sc: StringContext) extends AnyVal { |
| 177 | + def json(args: Any*): JSONObject = sys.error("TODO - IMPLEMENT") |
| 178 | +} |
| 179 | + |
| 180 | +def giveMeSomeJson(x: JSONObject): Unit = ... |
| 181 | + |
| 182 | +giveMeSomeJson(json"{ name: $name, id: $id }") |
| 183 | +``` |
| 184 | +{% endtab %} |
| 185 | + |
| 186 | +{% tab 'Scala 3' for=json-definition-and-usage %} |
| 187 | +```scala |
| 188 | +extension (sc: StringContext) |
| 189 | + def json(args: Any*): JSONObject = sys.error("TODO - IMPLEMENT") |
| 190 | + |
| 191 | +def giveMeSomeJson(x: JSONObject): Unit = ... |
| 192 | + |
| 193 | +giveMeSomeJson(json"{ name: $name, id: $id }") |
| 194 | +``` |
| 195 | +{% endtab %} |
| 196 | + |
| 197 | +{% endtabs %} |
| 198 | + |
| 199 | +In this example, we're attempting to create a JSON literal syntax using string interpolation. The `JsonHelper` implicit class must be in scope to use this syntax, and the json method would need a complete implementation. However, the result of such a formatted string literal would not be a string, but a `JSONObject`. |
| 200 | + |
| 201 | +When the compiler encounters the literal `json"{ name: $name, id: $id }"` it rewrites it to the following expression: |
| 202 | + |
| 203 | +{% tabs extension-desugaring class=tabs-scala-version %} |
| 204 | + |
| 205 | +{% tab 'Scala 2' for=extension-desugaring %} |
| 206 | +```scala |
| 207 | +new StringContext("{ name: ", ", id: ", " }").json(name, id) |
| 208 | +``` |
| 209 | + |
| 210 | +The implicit class is then used to rewrite it to the following: |
| 211 | + |
| 212 | +```scala |
| 213 | +new JsonHelper(new StringContext("{ name: ", ", id: ", " }")).json(name, id) |
| 214 | +``` |
| 215 | +{% endtab %} |
| 216 | + |
| 217 | +{% tab 'Scala 3' for=extension-desugaring %} |
| 218 | +```scala |
| 219 | +StringContext("{ name: ", ", id: ", " }").json(name, id) |
| 220 | +``` |
| 221 | +{% endtab %} |
| 222 | + |
| 223 | +{% endtabs %} |
| 224 | + |
| 225 | +So, the `json` method has access to the raw pieces of strings and each expression as a value. A simplified (buggy) implementation of this method could be: |
| 226 | + |
| 227 | +{% tabs json-fake-implementation class=tabs-scala-version %} |
| 228 | + |
| 229 | +{% tab 'Scala 2' for=json-fake-implementation %} |
| 230 | +```scala |
| 231 | +implicit class JsonHelper(val sc: StringContext) extends AnyVal { |
| 232 | + def json(args: Any*): JSONObject = { |
| 233 | + val strings = sc.parts.iterator |
| 234 | + val expressions = args.iterator |
| 235 | + val buf = new StringBuilder(strings.next()) |
| 236 | + while (strings.hasNext) { |
| 237 | + buf.append(expressions.next()) |
| 238 | + buf.append(strings.next()) |
| 239 | + } |
| 240 | + parseJson(buf) |
| 241 | + } |
| 242 | +} |
| 243 | +``` |
| 244 | +{% endtab %} |
| 245 | + |
| 246 | +{% tab 'Scala 3' for=json-fake-implementation %} |
| 247 | +```scala |
| 248 | +extension (sc: StringContext) |
| 249 | + def json(args: Any*): JSONObject = |
| 250 | + val strings = sc.parts.iterator |
| 251 | + val expressions = args.iterator |
| 252 | + val buf = new StringBuilder(strings.next()) |
| 253 | + while strings.hasNext do |
| 254 | + buf.append(expressions.next()) |
| 255 | + buf.append(strings.next()) |
| 256 | + parseJson(buf) |
| 257 | +``` |
| 258 | +{% endtab %} |
| 259 | + |
| 260 | +{% endtabs %} |
| 261 | + |
| 262 | +Each of the string portions of the processed string are exposed in the `StringContext`'s `parts` member. Each of the expression values is passed into the `json` method's `args` parameter. The `json` method takes this and generates a big string which it then parses into JSON. A more sophisticated implementation could avoid having to generate this string and simply construct the JSON directly from the raw strings and expression values. |
0 commit comments