新しくプログラミング言語を作る際に文字列型をどうするべきか
この記事は「言語実装 Advent Calendar 2025」の3日目の記事です。
この記事は、新しくプログラミング言語を設計する際に文字列型をどうするべきかについて、私の持論をまとめたものです。
以前「新しくプログラミング言語を作る際に数値型をどうするべきか」という記事を書きましたが、この記事はそれの文字列版です。
推敲が足りずに同じことを何箇所かで繰り返している場合がありますが、冗長性だと思ってご容赦ください……。
文字列とは何か
文字列とは何でしょうか?文字の列ですね。では計算機上での「文字」とは何でしょうか?
計算機に関するやり取りの中での「文字」は警戒すべき単語で、時と場合により以下のいずれかを指します:
- 7ビットまたは8ビットの整数で表される、符号化された文字(主にアルファベット)
- 16ビット整数で表される、UnicodeのBMPのコードポイント(サロゲートコードポイントを含む)
- UTF-16のコードユニットとも言う。
-
UnicodeコードポイントまたはUnicodeスカラー値
- Unicodeコードポイントは0以上0x10FFFF以下の整数値です。
U+の後に十六進表記を続けて表記されます。 - Unicodeスカラー値は、Unicodeコードポイントであって、0xD800以上0xDFFF以下の範囲に属さないものです。
- Unicodeコードポイントは0以上0x10FFFF以下の整数値です。
- Unicodeの書記素クラスター (grapheme cluster)
- 詳しい説明は省略しますが、プログラマーでない一般人が「文字」と考えるやつに近そうです。
- その他、なんらかの文字集合の文字をなんらかの符号化方式で表現した整数
よって、この記事では以後「文字」という用語はなるべく避けることにします。
Unicodeという名前が出てきましたが、Unicodeは現代のプログラミング言語が意識すべき唯一にして最重要の文字集合です。現代のプログラミング言語はUnicodeをうまく扱える必要があります。 100年後のIT業界がどうなっているかは分かりませんが、ここ10年以内に普及させたいプログラミング言語はなんらかのUnicodeサポートを盛り込むべきです。
「文字」という用語を避けるとして、文字列とは何なのかという問題に戻ります。バイト列としての表現は脇に置いて、抽象的に見たときに文字列が何を表すかは以下のいずれかに分類できそうです:
- 8ビット整数列
- 8ビット整数列とエンコーディングの組
- 16ビット整数列、ただしエンコーディングはUTF-16を意図する
- Unicodeコードポイントの列
- Unicodeスカラー値の列
1はプログラミング言語を作る側にとっては簡単で、単に8ビット整数列を文字列と呼ぶやり方です。C言語の char 配列やPHP, Luaなどの文字列が該当します。以下、この方式の文字列を「8ビット文字列」と呼ぶことにします。
言語によっては、8ビット文字列をUTF-8とみなして操作する関数を標準で提供していることがあります。GoやLuaが該当します。
2は8ビット文字列にエンコーディングの情報を持たせたものです。Rubyが採用しています。
3は文字列に任意の16ビット整数を格納できるようにしているものです。言うまでもなく、Unicodeが16ビットだった頃にUnicodeを採用した言語、あるいはその頃にUnicodeを採用したシステムの影響を強く受けた言語がこのパターンで、JavaやJavaScript, C#が該当します。これらの言語やランタイムをターゲットとする(トランスパイルする)言語もこれを踏襲していることが多いです。
4は文字列がUnicodeコードポイントの列であることが保証されているタイプです。3との違いは、 "\uD800\uDC00" と "\U00010000" を区別できることで、5との違いはサロゲートコードポイントを持てることです。Pythonの str が該当します。PythonはPEP 383でサロゲートコードポイントを有効活用しています。Haskellの String も該当します。
5は文字列がUnicodeスカラー値の列であることが保証されているタイプです。Rustの str や、Haskellの Text が該当します。Swiftの String もUnicodeスカラー値の列です。
1から5の分類とは別に、「文字列の中身にヌル文字 \0 を含められるか」という観点もあります。C言語の文字列はヌル文字を終端として扱うので、中身としてヌル文字を持てません。従って、char * は正確には「8ビット整数(ただし0を除く)の列」となります。C言語よりも新しい言語は文字列の途中にヌル文字を持てることが多いですが、OSのAPIはC文字列を期待していることが多いので注意が必要です。OSのAPIに文字列を渡すときは、中身にヌル文字を含まないことをチェックすべきでしょう。 例えば、書き込む先のファイル名にヌル文字が埋め込まれていたら、高級言語の関数でチェックした拡張子等とOSが実際に書き込む先のファイル名の拡張子が異なる、という潜在的な問題につながるかもしれません。
表現
文字列がメモリー上でどう表現されているかも重要であることが多いです。表すものがUnicode文字列であれば、UTF-8, UTF-16, UTF-32のいずれかで表現することになります。
最近の言語はUTF-8を採用するものが多いです。一方で、「JavaScriptにトランスパイルする」「JVMや.NETで動かす」ことを念頭に置くのであればUTF-16も選択肢に入るでしょう。UTF-32は馬鹿正直に実装するとたとえASCIIのみからなる文字列であっても1文字につき4バイト消費することになり、不利です。
Unicodeを扱える言語の文字列は、表現の観点から見ると大雑把には次の7通りに分類できます:
- UTF-8で表現&Unicodeスカラー値の列であることを保証する(例:Rust)
- UTF-8で表現&Unicodeスカラー値の列であることを保証しない(例:Go)
- UTF-16で表現&Unicodeスカラー値の列であることを保証する
- UTF-16で表現&Unicodeスカラー値の列であることを保証しない(例:JavaScript)
- UTF-32で表現&Unicodeスカラー値の列であることを保証する
- UTF-32で表現&Unicodeスカラー値の列であることを保証しない(例:Python)
- 内部表現は露出しないが、Unicodeスカラー値の列として利用できるようにする(エンコーディングは実装次第)
この節では、文字列の表現に関するトピックをいくつか見ていきます。
不変条件で保証するか否か
表現をUTF-8やUTF-16とする場合、型の不変条件としてUnicodeスカラー値の列としての正しさを保証するかどうかという論点があります。
型の不変条件として保証する場合、Unicodeの別のエンコーディングへの変換(UTF-8からUTF-16など)の際にエラー処理を考慮しなくて済むという利点があります。ただし、外部から与えられた文字列を受け取る際に検査が必要になります。
保証しない場合は、エンコーディングの変換の際にエラー処理を用意するか、適当な文字(典型的にはU+FFFD)で置き換えるなどの処理をする必要があります。
不変条件で保証する場合→エンコーディングを露出するか否か
内部表現を仮にUTF-8としたとして、それを内部実装にとどめるか、型の仕様に明記するかという論点があります。
内部実装にとどめる場合は、文字列の内部表現を変えてもAPI互換性を保てるという利点があります。Haskellの Text はバージョン2.0で内部表現をそれまでのUTF-16からUTF-8に変えましたが、大きなAPIの非互換はないはずです[1]。
このほか、複数のターゲットにコンパイルする言語の場合は、ネイティブコンパイルの際にはUTF-8を使い、JVMやJavaScriptにコンパイルする際はUTF-16を使うという戦略があり得るかもしれません。
型の仕様に明記する場合は、整数のインデックスを使った操作(コードユニットへのアクセス、検索、部分文字列等)を外部に露出できるというメリットがあります。
UTF-16またはUTF-32を採用する場合の工夫
文字列型の用途は様々ですが、ASCII文字列を効率的に扱えることが重要な場合は多いでしょう。すると、真面目に1コードポイントにつき2バイト(UTF-16の場合)、あるいは4バイト(UTF-32の場合)割り当てるのは馬鹿らしいです。
よって、「文字列の中身がすべて8ビットの範囲(ASCIIもしくはLatin-1)に収まる場合はバイト列として表現する」という戦略が考えられます。
UTF-32の場合は、「文字列の中身がすべて16ビット(BMP)に収まる場合は16ビット列として表現する」という戦略があり得ます。
こういう戦略を採用している処理系としては、Python, Java, Guileなどがあるようです。
- Python: PEP 393 – Flexible String Representation | peps.python.org
- Java: JEP 254: Compact Strings
- Latin-1 or UTF-16
- Guile: String Internals (Guile Reference Manual)
- Latin-1 or UTF-32
これらの戦略を採用する前提条件としては、「文字列がimmutableである」ことが実質的に必要です。mutableな文字列の場合は書き換えで「文字列の中身が8ビットまたは16ビットに収まる」という条件が崩れる可能性があり、「1文字書き換えようとしただけなのに文字列全体のコピーが走った」という事態になります。
もちろん、UTF-8を採用していればASCII文字列に対する最適化は必要ありません。こういう工夫が欲しくなるのは、UTF-16(歴史的事情)やUTF-32(コードポイントへのランダムアクセス)を採用する必要がある言語です。
文字列型の特性とバリエーション
用途によっては文字列に類する型を複数用意する必要があるかもしれません。よくHaskellが槍玉に挙がって「Haskellの文字列型が多すぎる」みたいに言われることがありますが、他の言語でもよく考えると文字列に類する型が複数ある場合があります。
表すものによるバリエーション:バイト列 vs Unicode
Python 3にはUnicode文字列を表す str 型の他にバイト列を表す bytes 型があります。表すものが違うから別の型になっているわけです。
JavaScriptではバイト列を Uint8Array で表現できますが、JavaScriptにコンパイルする言語を作る立場からすると、バイト列を文字列っぽく扱える関数がもう少し用意されていたらなあ、と思ったりします。
不変(immutable)にするかどうか
ある程度高級な言語(例えば、GCを備えるような言語)であれば、文字列型は不変(immutable)を基本とすべきだと筆者は思います。
immutableであれば、「同じ内容の文字列をメモリー上では同じ実体で表現する」という最適化(interning)が可能になり、メモリーの節約だったり比較の高速化が見込めます。例えば、Luaは伝統的に文字列をinternしていました(最近は長い文字列をinternしなくなったみたいな話も聞きますが)。
また、文字列がimmutableであれば、後述する部分文字列の最適化や、連結の効率化にも繋がります。
「文字列を書き換える用途もあるじゃないか」と思う人もいるかもしれませんが、文字列に対する「書き換え」の多くは末尾に対する追記なのではないでしょうか。例えば、UTF-8でエンコードされた文字列の途中を書き換えたくなるようなケースがどのくらいあるでしょうか。「文字列に対する末尾の追記を効率的に行いたい」という需要は、StringBuilder のような専用の型と関数で満たすべきだと思います。
ランダムアクセスが必要かどうか
「文字列の i 番目の文字(コードユニット)を高速に取得(ランダムアクセス)したい」という用途はどのくらいあるでしょうか。先頭から順に辿っていく使い方が多いのであれば、イテレーターのようなインターフェースを用意すれば十分ではないでしょうか。
イテレーターの提供で十分なのであれば、文字列型の実装を隠蔽して「内部的なエンコーディングは実装の詳細とする」ということが可能になります。
部分文字列の効率化(スライス)
文字列がimmutableであれば、部分文字列(スライス)を取得する際に内容をコピーするのではなく、大元の文字列への参照と範囲だけを保持するという表現が可能になります。
例えば、Haskellの ByteString 型や Text 型はコピーせずに部分文字列を取得できます。Standard MLには、普通の文字列型 string の他に、「文字列への参照と範囲」からなる substring 型があります。
C言語の文字列はヌル終端ですが、部分文字列は一般にはヌル終端にはならないので注意しましょう。「C言語との相互運用を考えて文字列のメモリー上での表現を常にヌル終端にする」という方針と「部分文字列を定数時間で作れるようにする」という方針は両立しません。
連結の効率化
文字列の連結というのはよくある操作です。
例えば、文字列連結演算子を <> とし、a と b が1000文字からなる文字列とし、a <> ", " <> b という文字列の連結を考えましょう。愚直に実行すると、
a <> ", " <> b
= "AA...AA, " <> b -- a <> ", " を表すために1002文字の領域を確保して内容をコピーする
= "AA...AA, BB...BB" -- 結果を表すために2002文字の領域を確保して内容をコピーする
となり、合計でメモリーの確保が3004文字分、そして文字列のコピーも3004文字分発生します。最終的な2002文字分は良いとして、途中の1002文字の確保とコピーは無駄です。効率化の余地があります。
優れた言語処理系は、文字列の連結が連なっているのを見たら、n項演算であるかのように取り扱って効率的に実行するべきです。例えば、Luaは文字列連結演算子 .. が連なっていたらまとめて扱うようにしているみたいです。
文字列の連結が静的に連なっていればコンパイラーの最適化でどうにかなるかもしれませんが、実際には
let s = "";
for (let i = 0; i < arr.length; ++i) {
s += arr[i];
}
のように文字列連結をループで回す、最適化しづらいコードが書かれるかもしれません。そういう場合は、文字列をメモリー上の連続した領域で表すのではなく、断片への参照を持つデータ構造を採用するべきかもしれません。ropeというやつです。
文字列構築の工夫:バッファー
さっきは文字列をループで結合するコードを出しましたが、言語処理系を単純に保ちたい人はropeのような工夫は採用しづらいかもしれません。そういう場合でも、プログラマーの工夫によって効率的な文字列構築ができる手段は用意しておくべきです。
例えば、Javaの StringBuilder のようなAPIを用意すると良いでしょう。
文字列ビルダーみたいなAPIが用意されていない言語でも、「文字列のリストの結合」(JavaScriptでいう Array.prototype.join みたいなやつ)は効率的に実装されている可能性があります。言語処理系を実装する側から見ると、「文字列のリストの結合」は効率的に実装しなければならない、という言い方もできます。
イテレーター
文字列に対するイテレーターは有用です。例えば、UTF-8でエンコードされた文字列をUnicodeスカラー値の列として解釈するイテレーターがあると便利です。便利というか、Unicode文字列を提供する言語処理系であれば、文字列をUnicodeスカラー値の列、あるいはUnicodeコードポイントの列として扱うイテレーターを提供すべきです。
書記素クラスター単位のイテレーターというものも考えられますが、書記素クラスターの解釈にはUnicodeのデータベースが必要なこと、要素が可変長(固定長整数ではなく、文字列)になることなどから、まずはUnicodeスカラー値のイテレーターを優先的に整備して、書記素クラスターはその上に実装する形になるでしょう。
関数型言語でイテレーターに類するAPIを提供する場合、fold のような高階関数を提供するか、次のような「文字列が空でない場合先頭文字と残りを Maybe に包んで返す」関数を提供することになるでしょう:
-- Haskell Text
uncons :: Text -> Maybe (Char, Text)
(* Standard ML *)
val getc : substring -> (char * substring) option
この場合の注意点として、「先頭文字を除いた残り」は文字列をコピーすることなく取得できる必要があります。そうしないと、文字列を走査するだけなのにコピーが走りまくって効率が悪いです。Haskellの Text 型はスライスになれるので良いですが、Standard MLの場合は substring というスライス型を使っています。
OSの文字列
OSと文字列をやりとりする場面は多いと思います。例えば、ファイル名、コマンドライン引数、環境変数など。
WindowsではOSの文字列は基本的にUTF-16(を意図する16ビット整数列)です。モダンな言語処理系を作るのであれば、うっかりANSI版のAPIを使ってShift_JISのようなレガシーな文字コードと格闘する羽目にならないようにしてください。
UnixではOSとやりとりする文字列は8ビット文字列です。エンコーディングはロケール依存だと思いますが、最近の環境ではUTF-8が多いでしょう。
一方で、ファイルの中身はOSやロケールに依存せずにUTF-8として扱いたい場面が最近は多いのではないでしょうか。Pythonの場合はUTF-8 Modeみたいなやつがあります(PEP 540 – Add a new UTF-8 Mode | peps.python.org)。
最初の方にも書きましたが、OSのAPIはヌル終端文字列とすることが多いです。ヌル文字を途中に含む文字列をOSに渡せるようにしてしまうと、アプリケーションとOSで見える文字列が違ってしまい、脆弱性の原因になるかもしれないので注意してください。
OSとのやりとりを効率化したい場合は、文字列のメモリー上の表現もヌル終端とすると良いかもしれません。実際、Luaはそうしていたと思います。ですが、これは前述のようにスライスの最適化とは両立しないので注意してください。
ケーススタディー:悪い例
ここでは、筆者が把握している限りで色々な言語の文字列事情を見てみます。まずは真似すべきでない、悪い例から。
C/C++: ワイド文字列
C/C++には「ワイド文字列」という概念がありますが、理念はともかく結果としては成功したとは言い難いです。
まず、プラットフォームによって内容が違います。Windowsでは16ビット文字列(UTF-16)なのに対して、他のプラットフォームではUTF-32のことが多いです。例外として、BSD libcではロケールがヨーロッパの8ビット圏の場合にその8ビットをそのまま利用することがあるようです。
また、ASCII主体の時にメモリー効率が悪いです。
最初に書いたように現代ではUnicodeだけが重要なので、モダンな言語ではワイド文字列のことは忘れて、Unicodeのどのエンコーディングを採用するべきか考えましょう。
ちなみに、C/C++以外だとStandard MLにもワイド文字列が規定されています。
C++の basic_string
C++の basic_string は文字の型をテンプレート引数に取れますが、私から見るとこの設計は良くなかったんじゃないかなあと思います。少なくとも、Unicode文字列は別の方式で定義するべきでした。
std::u8string (std::basic_string<char8_t>) を見てみましょう。これは
- 中身が正当なUTF-8であることが保証されていない
- UTF-16 (
std::u16string) やUTF-32 (std::u32string) に一発で変換するためのメンバー関数が生えていない- オブジェクト指向ってやつを使うとデータにメソッドを生やせて便利だってことをC++の中の人に誰か教えてあげて!
- 中身をコードポイントの列としてみなすためのiteratorがない
の三重苦です。
以前に「C++標準化委員会、ついに文字とは何かを理解する: char8_t」という記事がありましたが、私から見るとC++標準化委員会が「文字列とは何か」を理解しているとは言い難いです。
Haskellの String
Haskell標準の文字列型は、「文字 Char のリスト」として定義されています。Char は具体的にはUnicodeコードポイントです。
type String = [Char]
シンプルな定義に見えますが、Haskellのリストは連結リストであり、実は非常に空間効率が悪いです。具体的には64ビット環境では1文字あたり40バイト(5ワード)消費します。
実用的なHaskellコードでは、Text (Unicodeスカラー値の列)や ByteString (バイト列)が使われます。
Haxeの String
Haxeという言語は色々なターゲットにコンパイルできます。Java, JavaScript, Python, Lua, バイトコード、などなど。
Haxeの文字列型はターゲットにおいて最も自然な文字列型にマップされるので、ターゲットによって使用すべきUnicodeエンコーディングが異なります。JavaやJavaScriptならUTF-8、PythonならUTF-32、LuaならUTF-8という具合に。ASCIIしか使わない人なら問題ないのかもしれませんが。
Haxe側でエンコーディングを抽象的に扱える手段(イテレーター)があれば良いのでしょうが、私が触った2016年ごろはイテレーターも target.utf16 マクロも未整備で地獄でした。今はStringIteratorUnicodeというのがあるようです。
- Haxe を触ってみた感想(2016年4月)
- Haxe の正規表現のターゲット環境による違い(2016年4月)
- haxe.iterators.StringIteratorUnicode - Haxe development API
これと同じ理由(ターゲットによって表現が違う)で、C++製のクロスプラットフォームGUIツールキットであるwxWidgetsが提供する wxString 型もなかなか地獄でした。
PureScriptもJavaScriptバックエンドではJavaScript文字列を使いますが、ネイティブバックエンドではUTF-8を使うようなので、似たような問題を抱えていそうです。
ケーススタディー:普通の例
悪い例は程々にして、普通の例も見てみましょう。
JavaScript
JavaScriptの文字列はUTF-16を想定した16ビット整数列です。UTF-16としての正しさは保証されません。"\uDC00" のようなorphan surrogateが許容されます。
モダンなJavaScriptでUTF-8を扱いたい場合は、Uint8Array を使うというのが一つの手です。WebやNode.jsで使える TextEncoder/TextDecoder で文字列との相互変換ができます。mutableなのがちょっと残念ですが、immutable ArrayBufferが入ればimmutableにできるようになるのでしょうか?
Ruby
Rubyの文字列はバイト列です。エンコーディングを持てるようですが、私は最近のRuby事情に疎いのでUTF-8以外のエンコーディングがどの程度使われているのかは知りません。
特筆すべき点として、Rubyの文字列はデフォルトで可変です。freeze メソッドや frozen_string_literal コメントで不変な文字列を作ることができるようです。
irb(main):001> a = "Hello"
=> "Hello"
irb(main):002> b = a
=> "Hello"
irb(main):003> b << " world!" # bの末尾に追加
=> "Hello world!"
irb(main):004> b # bが更新される
=> "Hello world!"
irb(main):005> a # aも更新されている!
=> "Hello world!"
Python 3
Pythonの文字列 str はUTF-32を想定した約21ビットの整数列です。サロゲートコードポイント(U+D800-U+DFFF)も許容され、PEP 383ではサロゲートコードポイントの一部を8ビット文字列との相互運用のために積極的に活用しています。
UTF-32の文字を全て4バイトで表すのは非効率なので、PEP 393でコードポイントの最大値に応じて圧縮した表現を使えるようにしています。
- PEP 383 – Non-decodable Bytes in System Character Interfaces | peps.python.org
- PEP 393 – Flexible String Representation | peps.python.org
str 型の他に、bytes という不変なバイト列の型もあり、文字列記法 b"" が使えたりします。
Rust
Rustの文字列(str/String)はUTF-8エンコードされた文字列です。型の不変条件として正当なUnicode文字列であること(well-formedness)が規定されています。
Rust特有の事情として、所有権に応じて複数の型に分かれているようです。
OSと文字列をやりとりするための OsStr/OsString という型もあります。
Go
Goの文字列はUTF-8を想定した8ビット文字列です。
Windowsではちゃんとワイド版(UTF-16)のAPIを叩くのでUnicode文字を含むファイル名もちゃんと扱えるんだったと思います。
Lua
Luaの文字列は8ビット文字列です。構文としてUTF-8でエンコードする \u{} というエスケープシーケンスがあったり、Lua 5.3には utf8 というライブラリーが付属したりしますが、UTF-8以外の文字列も格納できます。
Luaの標準ライブラリーは全体的にCの標準ライブラリーの薄いラッパーという感じがしており、特にWindowsではANSI版のAPIを使うので、日本語環境ではShift_JISが使用されるかもしれません。
C#
C#の文字列は、Java、JavaScript、Windows NTと同様に、UTF-16を想定した16ビット整数列です。
C#の文字列は長いことUTF-16コードユニットと書記素クラスターだけが単位で、「Unicodeコードポイント?サロゲートペアをいい感じに扱ってくれるAPI?知らん!あ、書記素クラスターを扱うAPIならあるよ」みたいな感じでしたが、比較的最近になってUnicodeスカラー値を表す rune というデータ型ができたらしいです。「Microsoft、ついにUnicodeスカラー値とは何かを理解する」と言う記事を書く必要があるかもしれません。
- System.Text.Rune class - .NET | Microsoft Learn
- Introducing System.Rune · Issue #23578 · dotnet/runtime Runeの初出?
- TextElementEnumerator Class (System.Globalization) | Microsoft Learn Runeの導入前からある
Swift
Swiftの文字列 String は抽象的なUnicodeスカラー値の列で、Character は書記素クラスターに対応しているようです。
文字列からはUTF-8/UTF-16/UTF-32のビューを取得できます。
Swift 5で内部表現をUTF-8にしたという話が2019年ごろにありました。
ちなみに、Objective-C/Cocoaの NSString はUTF-16を基本としていました(インデックスがUTF-16コードユニットの単位となる)。
Haskell
Haskellには何種類かの文字列型があります。大まかに分けると
-
String: Unicodeコードポイントの列。空間効率が非常に悪く、標準であること以外取り柄がない。 -
Text: Unicodeスカラー値の列。最近内部表現がUTF-8になった。スライスにもなれる。 -
ByteString: 8ビット文字列。スライスにもなれる。
の3通りです。
Text と ByteString については、さらに「strict」「lazy」の区別があり、さらに構築用のbuilderが用意されています。lazyはファイル等から読み込んだ内容をメモリー上に全体を乗せることなく処理したい、みたいな状況で使えます。
Haskellの文字列については以前記事を書きました:
-
Haskellの文字列型:分類と特徴
-
Textの内部表現は執筆以降に変化しているので注意。
-
- Haskellの文字列型:変換時の心構えと変換方法まとめ
OCaml
OCamlの文字列は8ビットの不変な文字列です。以前は可変だったようです。Unicodeを扱いたい場合はUTF-8を使うことになるでしょう。
Unicodeスカラー値を表す Uchar.t という型も用意されているようです。
で、結局新言語での文字列型はどうするべきか
しがらみのない新言語を作る場合は、UTF-8を基本とするのが良いと思います。つまり、
- 文字列自体は任意の8ビット整数列として、ライブラリー等でUTF-8をいい感じに扱えるようにする(例:Go)
- 内部的にはUTF-8として、Unicodeスカラー値の列として見せる(例:Rust, Swift)
のいずれかです。
一方、しがらみがある場合、つまりJavaScript/JVM/.NETにコンパイルする言語を作る場合は、UTF-16を採用したいかもしれません。Scala, Kotlin, F#なんかがそうですね。
ただ、Webをターゲットとする場合でも、WebAssemblyをターゲットとする場合はUTF-8の方がJavaScriptとの相互運用がやりやすいかもしれません。具体的には、Uint8Array に格納したUTF-8文字列はJavaScript側の TextEncoder/TextDecoder で扱えるのに対し、Uint16Array をJavaScript文字列と変換する関数は提供されていません。
複数のターゲットを想定する言語、例えばJavaScriptとネイティブコードの両方を想定する場合は、文字列の扱いが一貫するようにするべきです。両方でUTF-8を採用するか、両方でUTF-16を採用するか、抽象化してUnicodeスカラー値の列として見せるか、の3択です。場合によっては複数の文字列型を提供しても良いかもしれません。
Windowsに対応させる場合、ワイド版のAPIを使ってUnicode文字列を正しくOSとやりとりするようにするべきです。
文字列型は不変を基本とするのが良いでしょう。文字列構築にはビルダー用のAPIを提供しましょう。関数型言語の場合はスライス型があった方がイテレーターを提供しやすいでしょう。
文字型はどうでしょうか。UTF-8などの可変長エンコーディングを前提にする場合は、「コードユニットに対応する型」を用意する必要はそこまでないでしょう。文字型が欲しかったらUnicodeコードポイントを表す型か、Unicodeスカラー値を表す型を用意しましょう。
私が開発しているLunarMLでの文字列事情もそのうち紹介できたらと思います。
-
もちろん、「高速にUTF-16表現を得られる」という性質が失われるので、ICUの関数を呼ぶ際のコストが増えます。 ↩︎
Discussion