Skip to content

Commit a43be51

Browse files
committed
Multi-dollar interpolation
1 parent d376a22 commit a43be51

File tree

1 file changed

+154
-0
lines changed

1 file changed

+154
-0
lines changed

proposals/dollar-escape.md

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
# Multi-dollar interpolation
2+
3+
* **Type**: Design proposal
4+
* **Authors**: Alejandro Serrano Mena
5+
* **Discussion**: [KEEP-375](https://github.com/Kotlin/KEEP/issues/375)
6+
* **Status**: Experimental expected for 2.1
7+
* **Prototype**: Implemented in [this branch](https://github.com/JetBrains/kotlin/compare/rr/serras/dollar-escape-3)
8+
* **Related YouTrack issue**: [KT-2425](https://youtrack.jetbrains.com/issue/KT-2425/Provide-a-way-for-escaping-the-dollar-sign-symbol-in-multiline-strings-and-string-templates)
9+
10+
## Abstract
11+
12+
We propose an extension of string literal syntax to improve the situation around `$` in string literals. Literals may configure the amount of `$` characters required for interpolation.
13+
14+
## Table of Contents
15+
16+
* [Abstract](#abstract)
17+
* [Table of Contents](#table-of-contents)
18+
* [Motivating examples](#motivating-examples)
19+
* [Single-line string literals](#single-line-string-literals)
20+
* [Additional requirements](#additional-requirements)
21+
* [Proposed solution](#proposed-solution)
22+
* [Alternatives](#alternatives)
23+
24+
## Motivating examples
25+
26+
Strings are one of the fundamental types in Kotlin, developers routinely create (parts of) them by using string literals. However, the current design has a few inconveniences, as witnessed by this [YouTrack issue](https://youtrack.jetbrains.com/issue/KT-2446/String-literals). This KEEP pertains to how to improve the situation around `$`. It is a non-goal to change any behavior of string literals, including indentation (or stripping thereof).
27+
28+
[Kotlin's multiline strings](https://kotlinlang.org/docs/strings.html#multiline-strings) are raw, that is, every character from the start to the end markers is taken as it appears. In particular, there are no escaping sequences (`\n`, `\t`, ...) as found in single-line strings. Still, `$` is used to mark interpolation. If you need that character in the string, the most often used workaround is to interpolate the character, leading to an awkward sequence of characters, like `${'$'}`.
29+
30+
One important use case is embedding some pieces of code in which `$` is required by the syntax. Here is a (non-exhaustive) list of languages where `$` appears quite often:
31+
32+
- [JSON Schema](https://json-schema.org/learn/getting-started-step-by-step) uses `$` to define schema parameters.
33+
- [GraphQL](https://graphql.org/learn/queries/#variables) requires variable names to be prefixed by `$`.
34+
- Shell scripts often use `$`, as highlighted in [this discussion](https://teamcity-support.jetbrains.com/hc/en-us/community/posts/360006480400-Write-literal-bash-script-in-kotlin-string-?page=1#community_comment_360000882020).
35+
36+
```kotlin
37+
val jsonSchema: String = """
38+
{
39+
"${'$'}schema": "https://json-schema.org/draft/2020-12/schema",
40+
"${'$'}id": "https://example.com/product.schema.json",
41+
"title": "Product",
42+
"description": "A product in the catalog",
43+
"type": "object"
44+
}
45+
"""
46+
```
47+
48+
It is desirable for string literals that embed a schema or script in those languages to not require any changes in comparison to a standalone file. As a result, some IDE features like [_Language Injections_](https://www.jetbrains.com/help/idea/using-language-injections.html#edit_injected_fragment) provide a better user experience.
49+
50+
Furthermore, the use of `${'$'}` as a workaround has additional (bad) consequences if in the future Kotlin implements a feature akin to string templates. That `'$'` character would appear as one of the interpolated values, instead of as "static part" of the string.
51+
52+
### Single-line string literals
53+
54+
Single-line strings have their own ways of escaping the `$` character, namely a backslash,
55+
56+
```kotlin
57+
val order = Order(product = "Guitar", price = 120)
58+
59+
val amount = "${order.product} costs \$${order.price}"
60+
println(amount)
61+
// Guitar costs $120
62+
```
63+
64+
However, having a way for the `$` character to appear verbatim has some use cases. The main one is [better interoperability with i18n software](https://youtrack.jetbrains.com/issue/KT-7258/String-interpolation-plays-badly-with-i18n-and-string-positioning). For example, GNU `gettext` requires `%n$` to appear [verbatim in program source](https://www.gnu.org/software/gettext/manual/html_node/c_002dformat-Flag.html). This is currently not possible, since escaping is only available using `\$`.
65+
66+
```kotlin
67+
String.format(tr("Could not copy the dropped file into the %1\$s application directory: %2\$s"), a, b)
68+
```
69+
70+
Furthermore, we prefer to have fewer differences between the different kinds of string literals in the language.
71+
72+
### Additional requirements
73+
74+
Two additional requirements inform our proposed solution. First, any proposed solution must _not_ change the meaning of any existing string literal.
75+
76+
Second, interpolation must still be available in some form. For example, we would like the following code, in which `title` is computed from the members of the receiver `KClass`, to be expressible in the new syntax.
77+
78+
```kotlin
79+
val KClass<*>.jsonSchema: String
80+
get()= """
81+
{
82+
"${'$'}schema": "https://json-schema.org/draft/2020-12/schema",
83+
"${'$'}id": "https://example.com/product.schema.json",
84+
"title": "${simpleName ?: qualifiedName ?: "unknown"}",
85+
"type": "object"
86+
}
87+
"""
88+
```
89+
90+
## Proposed solution
91+
92+
> The solution is inspired by [C# 11's raw string literals](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-11.0/raw-string-literal#detailed-design-interpolation-case).
93+
94+
Every string literal, single- or multiline, may be **prefixed** by a sequence of one or more `$` characters, before the quotes.
95+
96+
* Single-line literals begin with `"`, and multiline literals with `"""`, as currently.
97+
* No character is allowed between the block of `$` characters and the first quote.
98+
99+
Using a single `$` as the prefix is allowed, but should result in a warning.
100+
101+
Having more than one `$` character prefixing the string changes **interpolation**. Instead of a single `$`, interpolation is done using the same amount of `$` characters as in the prefix.
102+
103+
* For string literals without a `$` prefix, the rule stays the same. That is, one `$` character is used.
104+
* In single-line literals, `\$` does _not_ count as one of the characters for interpolation. For example, `$$"$\$$hello"` represents the value `$$$hello`; the first dollar is not enough to start interpolation, `\$` is taken as a verbatim dollar, and the final dollar is again not enough to start interpolation.
105+
106+
Using this rule, the definition of `jsonSchema` for a `KClass` reads as follows.
107+
108+
```kotlin
109+
val KClass<*>.jsonSchema: String
110+
get()= $$"""
111+
{
112+
"$schema": "https://json-schema.org/draft/2020-12/schema",
113+
"$id": "https://example.com/product.schema.json",
114+
"title": "$${simpleName ?: qualifiedName ?: "unknown"}",
115+
"type": "object"
116+
}
117+
"""
118+
```
119+
120+
We can also satisfy the requirements of GNU `gettext` of `$` appearing verbatim.
121+
122+
```kotlin
123+
String.format(tr($$"Could not copy the dropped file into the %1$s application directory: %2$s"), a, b)
124+
```
125+
126+
Blocks of consecutive `$` characters longer than the prefix should be understood as a block of `$` characters followed by an interpolation.
127+
128+
```kotlin
129+
val amount = $$"$${order.product} costs $$${order.price}"
130+
println(amount)
131+
// Guitar costs $120
132+
```
133+
134+
We acknowledge that this solution does _not_ solve the problem of escaping (three or more) `"` characters inside a multiline string. The workaround is using `${"\"\"\""}`, or similar code which interpolates a single-line string with the three symbols.
135+
136+
## Alternatives
137+
138+
Apart from the proposed solution, some alternatives have been considered. This section describes them and the reason why they have been rejected.
139+
140+
**Keep the current status quo.** As mentioned in the _Motivating examples_ section, using `${'$'}` to escape the dollar character in multiline strings interacts badly with potential string templates. In particular, something that should be thought of as a "static part" of the string template appears as one of the "dynamic parts".
141+
142+
**Add escaping syntax without a prefix.** There are many syntactical possibilities, like `${}` or `$_`, but all share the fact that they would be added to multiline strings. The main problem here is backward compatibility: whereas before `$_` represented a dollar and an underscore, now simply represents a dollar. This means we would need a long period before making the actual change. In contrast, the proposed solution works right away, since the new escaping mechanism is opt-in: you need to prefix the string with some dollar character to be able to escape it.
143+
144+
**IDE-assisted replacement.** Another solution is to use a character different from `$`, like `%`, and then programmatically replace the latter with the former, `.replace('%', '$')`. This could even be assisted by the IDE, in the same way that IntelliJ now suggests adding `.trimIndent()` to multiline strings. The main problem is the interaction with interpolation, since the `.replace` call affects also the interpolated values; however, this is oftentimes not the intended behavior.
145+
146+
**Escaping quotes.** A previous iteration of this proposal also allowed to begin a multiline string literal with a number of `"` characters different than three, which had to be matched to end the string literal. However, this would be different to the behavior of current multiline strings,
147+
148+
```kotlin
149+
val thing = """"thing""""
150+
println(thing)
151+
// "thing"
152+
```
153+
154+
This behavior is [used quite often](https://github.com/search?q=%22%22%22%22+language%3AKotlin&type=code&ref=advsearch). Although we could provide the new behavior only when the string literal also begins with `$`, it would break uniformity across the different kinds of literals.

0 commit comments

Comments
 (0)