kagamihogeの日記

kagamihogeの日記です。

JEP 326: Raw String Literalsをテキトーに訳した

http://openjdk.java.net/jeps/326

JEP 326: Raw String Literals

Owner    Jim Laskey
Created 2018/01/23 15:40
Updated 2018/04/03 18:29
Type    Feature
Status  Candidate
Component   specification / language
Scope   SE
Discussion  amber dash dev at openjdk dot java dot net
Effort  M
Duration    M
Priority    3
Reviewed by Alex Buckley
Endorsed by Brian Goetz
Release tbd_major
Issue   8196004

Summary

Java言語にraw string literalsを追加します。raw string literalは複数行のソースコードに展開可能で、\nや、\uXXXX形式のユニコードエスケープなど、エスケープシーケンスを解釈しません。

Goals

  • 以下により開発者の効率を高めます。
    • Javaのインジケータを使わない、読みやすい形式の文字列表現
    • supply strings targeted for grammars other than Java
    • 改行用の特別なインジケータを使わずに複数行のソースに展開できる文字列
  • raw string literalsは従来の文字列リテラルと同等な文字列を表現可能、ただしプラットフォーム固有の改行は除く。
  • エスケープの現行のjavac文字列リテラル処理および左マージンのトリムと同様なことをするライブラリ

Non-Goals

  • 新規のString演算子は導入しない。
  • Raw string literalsは文字列補間をすぐにはサポートしません。将来のJEPで導入する可能性はあります。
  • 従来の文字列リテラルには変更はありません。これには以下を含みます。
    • 開始・終了ダブルクオートの繰り返しによるデリミタのカスタマイズ
    • エスケープシーケンスの処理

Motivation

エスケープシーケンスはJavaを含め多くのプログラミング言語で定義されており、直接書くのが難しい文字を表現するのに使います。たとえば、エスケープシーケンス\nはASCIIの改行を意味します。二行で"hello"と"world"を表示するには、文字列"hello\nworld\n"を使います。

System.out.print("hello\nworld\n");

以下のような表示になります。

hello
world

可読性に難点がある他、この例はUNIXベースのシステムをターゲットにしていますが、それ以外のOSでは\r\n(Windows)など異なる改行表現を使用する場合があります。Javaには、printlnなど高レベルのメソッドがあり、これはプラットフォームに適切な改行文字を使用します。

System.out.println("hello");
System.out.println("world");

GUIライブラリで"hello"と"world" を表示する場合、制御文字は意味が無い場合があります。

エスケープシーケンス、バックスラッシュ、はJavaの文字列リテラルでは\\です。二重のバックスラッシュはLeaning Toothpick Syndrome*1を生み出し、過剰なバックスラッシュが文字列の理解を難しくさせます。Javaデベロッパは以下のような例を良く使います。

Path path = Paths.get("C:\\Program Files\\foo");

ダブルクオート文字を使うための\"などのエスケープシーケンスも同様に非Javaプログラマが見る場合に理解を難しくさせます。例えば、ダブルクオートを含む文字列を検索するには以下のようになります。

Pattern pattern = Pattern.compile("\\\"");

実際のところ、エスケープシーケンスは例外的事項でありJavaの日常的な開発に出てくるものではありません。制御文字を使う機会は少なく、エスケープの存在は可読性とメンテナンス性に悪影響を及ぼします。Once we come to this realization, the notion of a non-interpreted string literal becomes a well reasoned result.

現実のJavaコードには、他のプログラム(SQL, JSON, XML, 正規表現など)のコードを埋め込むことがあり、これらは、ユニコードエスケープ・バックスラッシュ・改行を除くと、リテラル文字列そのままでキャプチャされる仕組みを必要とします。

本JEPの提案は、raw string literal、という新しい種類のリテラルを提案します。Javaエスケープと行端仕様とは別途で、多くの状況下で既存の文字列リテラルよりも可読性とメンテナンス性に優れる文字列を提供します。

File Paths Example

Traditional String Literals

Runtime.getRuntime().exec("\"C:\\Program Files\\foo\" bar");

Raw String Literals

Runtime.getRuntime().exec(`"C:\Program Files\foo" bar`);

Multi-line Example

Traditional String Literals

String html = "<html>\n" +
              "    <body>\n" +
              "          <p>Hello World.</p>\n" +
              "    </body>\n" +
              "</html>\n";

Raw String Literals

String html = `<html>
                   <body>
                       <p>Hello World.</p>
                   </body>
               </html>
              `;

Regular Expression Example

Traditional String Literals

System.out.println("this".matches("\\w\\w\\w\\w"));

Raw String Literals

System.out.println("this".matches(`\w\w\w\w`));

Output:

true

Polyglot Example

Traditional String Literals

String script = "function hello() {\n" +
                "   print(\'\"Hello World\"\');\n" +
                "}\n" +
                "\n" +
                "hello();\n";
ScriptEngine engine = new ScriptEngineManager().getEngineByName("js");
Object obj = engine.eval(script);

Raw String Literals

String script = `function hello() {
                    print('"Hello World"');
                 }
hello();
            `

ScriptEngine engine = new ScriptEngineManager().getEngineByName("js");
Object obj = engine.eval(script);

Output:

"Hello World"

Database Example

Traditional String Literals

String query = "SELECT `EMP_ID`, `LAST_NAME` FROM `EMPLOYEE_TB`\n" +
               "WHERE `CITY` = ‘INDIANAPOLIS'\n" +
               "ORDER BY `EMP_ID`, `LAST_NAME`;\n";

Raw String Literals

String query = ``
                 SELECT `EMP_ID`, `LAST_NAME` FROM `EMPLOYEE_TB`
                 WHERE `CITY` = ‘INDIANAPOLIS'
                 ORDER BY `EMP_ID`, `LAST_NAME`;
               ``;

Description

raw string literalはリテラルの新しい形式になります。

Literal:
  IntegerLiteral
  FloatingPointLiteral
  BooleanLiteral
  CharacterLiteral
  StringLiteral
  RawStringLiteral
  NullLiteral

RawStringLiteral:
  RawStringDelimiter RawInputCharacter {RawInputCharacter} RawStringDelimiter

RawStringDelimiter:
    ` {`}

raw string literalは1文字以上をバッククオート` (\u0060) (backquote, accent grave)のシーケンスで囲みます。raw string literalは1つ以上のバッククオートで開始し、同数のバッククオートで終了します。異なる個数のバッククオートは文字列の一部として扱います。

raw string literal内へのバッククオートの埋め込みは開始終了のバッククオートの数を増減させることで対応可能です。

raw string literalの文字は、CRとCRLFを除き、解釈を行いません。CR(\u000D)とCRLF(\u000D\u000A)は常にLF(\u000A)に変換されます。この変換はプラットフォーム間の振る舞いの驚きを最小にするためです。

バッククオートの開始があるが対応する終了が無い場合コンパイル時エラーになります。

Java言語仕様では従来の文字列リテラルには二種類のエスケープ、ユニコードエスケープとエスケープシーケンス、を規定しています。Raw string literalsはエスケープを解釈しません。つまり、エスケープ文字列はそのままになります。

\uxxxx形式のユニコードエスケープは、字句解析による解釈前に文字列入力の一部として処理されます。raw string literalの要求をサポートするために、字句解析がバッククオートの開始を見つけたらユニコードエスケープ処理を無効化し、終了したら再度有効化します。一貫性を保つため、ユニコードエスケープ\u0060は、バッククオートの代替としては使いません。

以下はraw string literalsの例です。

`"`                // a string containing " alone
``can`t``          // a string containing 'c', 'a', 'n', '`' and 't'
`This is a string` // a string containing 16 characters
`\n`               // a string containing '\' and 'n'
`\u2022`           // a string containing '\', 'u', '2', '0', '2' and '2'
`This is a
two-line string`   // a single string constant

classファイルでは、ある文字列定数がraw string literalか従来の文字列リテラル由来かどうかは記録しません。

従来の文字列リテラル同様、raw string literalは常にjava.lang.String型です。raw string literals由来の文字列も、従来の文字列リテラル由来の文字列と同様の扱いです。

Escapes

エスケープシーケンスを解釈しない複数行文字列を開発者が望む可能性は高いです。この要求に応えるため、Stringクラスにエスケープシーケンスを実行時に解釈するインスタンスメソッドを追加します。

public String unescape()

このメソッドは、JLSが定義しているエスケープシーケンス(3.3 Unicode Escapes, 3.10.6. Escape Sequences for Character and String Literals)と同一スペルの\を頭につけた文字を、そのエスケープシーケンスが表現する文字に変換します。

例(b0からb3はtrueになる)

boolean b0 = `\n`.equals("\\n");
boolean b1 = `\n`.unescape().equals("\n");
boolean b2 = `\n`.length == 2;
boolean b3 = `\n`.unescape().length == 1;

エスケープ変換を細かく制御できるメソッドを提供します。

また、エスケープ反転ツール用のメソッドもあります。以下のメソッドもStringに追加します。

public String escape()

このメソッドは、' 'より小さいすべての文字をUnicodeかcharacter escape sequencesに変換し、'~'より上の文字はUnicode escape sequencesに変換、", ', \エスケープシーケンスに変換します。

例(b0からb3はtrueになる)

boolean b0 = "\n".escape().equals(`\n`);
boolean b1 = ``.escape().equals(`\u2022`);
boolean b2 = "•".escape().equals(`\u2022`);
boolean b3 = !"•".escape().equals("\u2022");

Source Encoding

ソースファイルに非ASCII文字がある場合、javacコマンド(javac -encoding)で正しいエンコーディングを使う必要があります。もしくは、raw stringに適切なUnicodeエスケープをし、Unicodeエスケープを適切な非ASCIIに変換するライブラリを使用します。

Margin Management

複数行文字列の課題の一つに、左マージン(いわゆるヒアドキュメント)か、周囲のコードのインデントで文字列をフォーマットするか、があります。理想的には、文字列は周囲のコードとブレンドさせたいです。よって問題は、余分な左スペースの扱い方をどうするか、になります。

柔軟に対処出来るようにするため、raw string literalsのマージンはスキャンされます。余分な左スペースをトリムするメソッドをStringに追加する予定です。

Alternatives

Choice of Delimiters

従来の文字列リテラルとraw string literalは両方ともデリミタで文字シーケンスを囲みます。従来の文字列リテラルは開始・終了のデリミタにダブルクオート文字を使います。この対称性によりリテラルの読み込みとパースは簡単です。raw string literalも対称性のあるデリミタを用いますが、別の異なるデリミタを使う必要があり、これは文字シーケンス内にエスケープを付与しないダブルクオートを使う可能性があるためです。raw string literalのデリミタの選択には以下の考慮事項があります。

  • デリミタは短い文字列・マージン管理・一般的な可読性で効果的であるように大仰なものにならないこと。
  • 開始デリミタはその後にraw string literalのボディが続くことを明確に示すこと。
  • 終了デリミタは文字列にあまり出てこないものなこと。終了デリミタが文字列内に出現する場合、終了デリミタの埋め込みルールは明確かつシンプルなこと。埋め込みはエスケープ無しの実現が必須です。

デリミタには今のところ3つのLatin1文字、single-quote, double-quote, and backtick、としています。それ以外は明快さに影響を及ぼし、従来の文字列リテラルとの間に一貫性を欠くと考えています。

従来の文字列リテラルとraw string literalとに違いを出す必要があります。ダブルクオート以外の文字かカスタムフレーズである種の複合デリミタを作ることにより、raw string literalsでダブルクオートを使うことは出来ます。たとえば$"xyz"$abcd"xyz"abcdなどです。これら複合デリミタは基本的な要件は満たしますが、明快さに欠け、終了デリミタの埋め込みはシンプルではありません。Also, there is a temptation in the custom phrases case to assign semantic meaning to the phrase, heralding another industry similar to Java annotations.

連続クォート、"""xyz"""、について。これについては曖昧さを回避するのが面倒です。たとえば"" + x + ""は、従来文字列リテラル+変数+従来文字列リテラルの結合としても、" + x + "という7文字の raw string literalとしてでもパースが可能です。

バッククオートの利点は別の目的で使われない点です。連続クオートと空文字で発生する曖昧さを回避できます。Java言語仕様の用語における新しいデリミタになります。バッククオートは、シンプルな埋め込みルールを含め、デリミタの要求をすべて満たします。

デリミタの選択における別の考慮事項は将来の技術発展の可能性です。rawおよび従来の文字列リテラルの両方でシンプルなデリミタを使用するようなことが、将来の技術では出来るかもしれません。

このJEPではバッククオートを提案します。言語の現在のクオートとは異なりますが、同様の目的を果たします。

Multi-line Traditional String Literals

このオプションはraw string literalとは別モノですが、raw string literalsに加えて従来の文字列リテラルに複数行機能を持たせることも合理的かもしれません。この機能を有効化すると従来の文字列リテラルの複数行をエラーにするツールとテストに影響が出る可能性があります。

Other Languages

Javaは、raw stringsを言語レベルでサポートしない、数少ない現代的なプログラミング言語グループの1つに取り残されています。

以下の言語、C, C++, C#, Dart, Go, Groovy, Haskell, Java, JavaScript, Kotlin, Perl, PHP, Python, R, Ruby, Scala, Swift、はraw string literalsをサポートしており、デリミタとrawおよび複数行文字列の使用方法について調査しました。Unixツールのbash, grep, sedの文字列表現も調査しました。

複数行リテラル問題の解消には、従来の文字列リテラルのダブルクオートのボディでCRとLFを使えるようにJava仕様を変更する、という手法もあります。ただし、そういうダブルクオートの使い方ではエスケープの解釈が必要です。

異なる解釈の振る舞いを明示するには、異なるデリミタが必要です。Java以外の言語では様々なデリミタを採用しています。

Delimiters Language/Tool
"""...""" Groovy, Kotlin, Python, Scala, Swift
`...` Go, JavaScript
@"..." C#
R"..." Groovy (old style)
R"xxx(...)xxx" C/C++
%(...) Ruby
qq{...} Perl

Python, Kotlin, Groovy and Swiftはraw stringsにはダブルクオート3つを使います。これは既存の文字列リテラルとの連続性を反映したものです。

Go and JavaScriptはバッククオートです。文字列であまり使われない文字を選択しています。Markdownとの相性は良くないですが、大半の場合では問題ありません。

異色どころでは、C#@"..."などのメタタグは本JEPで提案するバッククオートと機能的に似ています。しかし、@Javaではアノテーションを示唆します。そうしたメタタグの使用法を持ち込むことは、将来的にそのメタタグの使用を制限します。

Heredoc

raw stringsのクオートの別案にヒアドキュメント*2があります。ヒアドキュメントはUnixシェルで最初に使われ始め、Perlなどに導入されました。ヒアドキュメントにはプレースホルダとエンドマーカがあります。プレールホルダは文字列をコード内に挿入する場所とエンドマーカを指定します。エンドマーカ―は文字列の最後につけます。

System.out.println(<<HTML);
<html>
    <body>
        <p>Hello World.</p>
    </body>
</html>
HTML

ヒアドキュメントはraw stringsの一つの案ではありますが、アナクロに思われます。さらにマージン管理の問題がやはり発生して複雑化します。

Testing

従来の文字列リテラルをraw stringリテラルで置き換えたテストのコピーを作成し、Stringのテストスイートを拡張します。

行端およびコンパイル単位の終了時におけるコーナーケースのテストをネガティブテストに追加します。

エスケープとマージン管理のメソッドのテストを追加します。

Risks and Assumptions

本JEPの仮定として、Markdown, Go, JavaScriptを含むraw string literalsにはバッククオートがあまり出現せず、そのため、バッククオートを繰り返すデリミタなら他よりも煩わしくない、としています。

*1:Leaning Toothpick、つまり斜めに傾けたつまようじが大量に並んでる様を揶揄する言葉

*2:"here" documents or heredocsが原文なんだけど「ヒアドキュメント」一つにまとまてしまった