Java 15 のテキストブロックを横目に C# 11 の生文字列リテラルを眺めて ECMAScript String dedent プロポーザルを想う
さりげに複雑な複数行文字列リテラル仕様
プログラミング言語における複数行文字列リテラルの仕様は、一見した印象より決めなければならないことが多く、さりげに複雑になりがちです。
例として、 Java 15 のテキストブロックと C# 11 の生文字列リテラルを比較してみましょう(C# 11 の生文字列リテラルには単一行形式と複数行形式がありますが、ここでは複数行形式の仕様のみ扱います)。
Java テキストブロック | C# 生文字列リテラル | |
---|---|---|
開始・終了デリミター | 開始・終了ともに """ (ダブルクォート 3 つ)開始側は改行必須 |
""" 、 """" 、 """"" などダブルクォート 3 つ以上(開始・終了で合わせる)開始側・終了側ともに改行必須 変数を埋め込む場合は開始デリミターに 1 つ以上のドル記号 $ をつけて $""" 、 $$""" 、 $$""""" のようにする |
最初と最後の改行を文字列値に含めるか | 最初の改行は含めない、最後の改行は含める (最後の改行を含めたくない場合はコンテンツ末尾行で改行せずに """ で閉じる) |
含めない |
エスケープシーケンスの扱い | エスケープする (たとえばコード上の \n はラインフィードになる) |
エスケープしない (たとえばコード上の \n はそのまま文字 \ と n になる) |
改行キャンセルの可否 | できる(行末バックスラッシュ) | できない |
文字列値中の改行文字 | CRLF は LF に置換 | コード上の改行文字のまま |
行頭の空白文字(インデント)を(どこまで)削除するか | インデントの最も少ない行がインデントゼロになるように各行のインデントを削除する | 終了デリミターがインデントゼロになるように各行のインデントを削除する (終了デリミターよりインデントが少ない行があるとコンパイルエラー) |
行末の空白文字を削除するか | 削除する エスケープシーケンス \s は空白文字に置換されて残るので必要ならこれを利用する改行キャンセル時の行末バックスラッシュまでの空白文字は削除しない |
削除しない |
式(変数)の埋め込み可否、埋め込み構文 | 埋め込めない | 埋め込める 式を埋め込む場合は、開始デリミターに 1 つ以上の $ をつけ、その $ と同じ数の波括弧 { } で式を括る(たとえば開始デリミターが $$$"""" であれば、変数 x の埋め込みは {{{x}}} となり、 2 つまでの波括弧 {{ や }} はそのままエスケープなしで含めることができる)
|
複数行文字列リテラルのデリミターをエスケープなしで含められるか | 含められない(要エスケープ) | 含められる たとえば """ (ダブルクォート 3 つ)を含めるにはデリミターを """" (4 つ)にする |
どちらも """
で始まり """
で終わる複数行文字列リテラルの表現であり、一見するとよく似ています。が、細部を見れば全く異なることがわかると思います。プログラマーはこれら細かい仕様(差異)を意識する必要があり、そうでなければ時折「エ゙ッ!?」と声を出して驚くことになります。
※ 記事執筆時点で C# の生文字列リテラルを使うには、 Visual Studio 2022 バージョン 17.2 Preview 1 で、プロジェクトファイルに <LangVersion>preview</LangVersion>
を記述する必要がありました(Mac 版 17.2 Preview は未提供でした)。
コンテンツにデリミターを含めるために
プログラミング言語 X でプログラミング言語 X のコードを生成したい、といった要求はまれによくあるものです。文字列リテラルを含むコードを文字列リテラルで表現する際、デリミターが工夫されていないとエスケープが必要になります。本来コードのコピペで済ませたいわけですが、コピペしたあとエスケープして回るハメになります。
// JavaScript
// テンプレートリテラルを含むコードをテンプレートリテラルで表現するにはエスケープが必要です。
eval(`
const answer = 42;
console.log(\`The answer is \${answer}.\\n\`);
`);
// エスケープシーケンスを無視するために String.raw を使うと、これまた面倒なことになります。
eval(String.raw`
const answer = 42;
console.log(${"`"}The answer is ${"$"}{answer}.\n${"`"});
`);
- C# の生文字列リテラル では、上述の通り、コンテンツに 3 つの連続したダブルクォート
"""
を含めたいならデリミターをダブルクォート 4 つ""""
にします。変数埋め込み用のデリミター{
}
の数も開始デリミターの$
の数で調整でき、表現力が高いです。 - Markdown のコードブロック の開始デリミターは
```
、````
、`````
など、 3 つ以上のバックティックです。終了デリミターは開始デリミターと同じかそれ以上の数のバックティックになります。コンテンツに```
を含めたいならデリミターを````
にします。 - Swift の Extended String Delimiters は、
#"..."#
のように、ダブルクォートの外側に 1 つ以上の#
をつけることで、エスケープシーケンスを無視して(エスケープせず)生文字列を記述する機能です。##"..."##
のように#
の数を増やすことで、コンテンツに#"
を含めることができます。 - Ruby のヒアドキュメント、 D 言語の Delimited Strings、 C++ の生文字列リテラル では、デリミターに任意の識別子を含めることができます。
ECMAScript – String dedent プロポーザル
ECMAScript 2015 に導入されたテンプレートリテラルでは、行頭の空白文字(インデント)が文字列値にそのまま残ります。一方で、インデントを解除したいという要求は確実にあり、インデント解除のためのテンプレート用タグを提供するライブラリ Dedent のダウンロード数は 1 週間あたり 10,000,000 件以上に達しています。類似ライブラリもぎょーさんあります。 そして新しい構文の提案も(何年も前から)あります。
- tc39/proposal-string-dedent: TC39 Proposal for multi-backtick templates with automatic margin stripping
https://github.com/tc39/proposal-string-dedent
いくつか構文案が挙がっていますが、個人的に興味深いと思ったのは、デリミターに ```
を採用した場合に発生する、 ECMAScript のバージョンによって異なる挙動を持つコード例です。
- Syntax proposals (meta issue) · Issue #40 · tc39/proposal-string-dedent
https://github.com/tc39/proposal-string-dedent/issues/40#issuecomment-1030527436
x = () => x;
x
```
`
alert("am i an inert string ... or am I eval? depends on your browser, if single backticks don’t need escaping!");
x
`
```
このような問題をうまく避けつつ、他の言語仕様を参考にして、できれば C# の生文字列リテラルのように表現力の高い仕様になってほしい気持ちです(dedent (インデント解除)という名のプロポーザルにどこまで期待するのでしょう)。
2022-04-01 追記
3 月の TC39 ミーティングで String dedent 用の構文追加に反対されたようで、専用構文ではなく単なるタグ関数になりそうです。
- Convert to API, remove mentions of syntax by jridgewell · Pull Request #47 · tc39/proposal-string-dedent
https://github.com/tc39/proposal-string-dedent/pull/47
その他の記事
Other Articles
関連職種
Recruit