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