Writer: Takuma Sato 日付: December 11, 2025
ミツモアのエンジニアの佐藤(@tkmsgr)です。
皆さんは空文字で検索して全件返ってきた経験はありますでしょうか? 私は810万回ぐらいあります。 これは部分文字列に検索ワードを含むものを返すという実装で見られます。
なぜ文字列に空文字が含まれているの?という説明を寄り道しながらしていこうと思います。
(Googleで空文字検索したらトップページにリダイレクトされるようです)
まず、本当に空文字を含むの?
そう思った方もいらっしゃるでしょう。 なので、適当なプログラム言語で実験してみます。
"aiueo".includes("") // true (JavaScript)
"aiueo".__contains__("") # True (Python)
"aiueo".contains("") // true (Java)
"aiueo".contains("") // true (Rust)
strings.Contains("aiueo", "") // true (Go)
str_contains("aiueo", "") // true (PHP)
SELECT position('' in 'aiueo'); -- 1 (PostgreSQL)
string.find("aiueo", "") -- nil (Lua) 例外パターン
なんかよう分からんですが、まあだいたい含んでそうですね(適当)
特徴的でもない “aiueo” という文字列を特別扱いしているというのは考えにくいです。
なので、大体の文字列は空文字を含んでいそうだと仮説を立てるのは自然に見えます。
本当に全部?
とはいえ上のものは完全な証明になっていないので、なんだかムズムズしますね。
そこで最終兵器を用意しました。
*// 有効なUnicodeコードポイントの組み合わせの結合で検証* function checkIncluded(text) { for (let あ = 0; あ <= 0x10FFFF; あ++) { for (let い = 0; い <= 0x10FFFF; い++) { for (let う = 0; う <= 0x10FFFF; う++) { for (let え = 0; え <= 0x10FFFF; え++) { for (let お = 0; お <= 0x10FFFF; お++) { const str = String.fromCodePoint(あ) + String.fromCodePoint(い) + String.fromCodePoint(う) + String.fromCodePoint(え) + String.fromCodePoint(お); if (!str.includes(text)) { console.log`反例発見🕵: U+${あ.toString(16)} U+${い.toString(16)} U+${う.toString(16)} U+${え.toString(16)} U+${お.toString(16)}`; return false; } } } } } } return true; } checkIncluded("");
もし反例を発見した方がいましたら、コメントで教えてください😊
演繹的なアプローチ(気になる人向け)
言語仕様を確認し、そこでも明示的ではない場合は実装を確認していきます。
今回は一例としてJavaScriptの言語仕様を確認していきます。
String.prototype.includes の ECMAScript仕様
https://tc39.es/ecma262/#sec-string.prototype.includes
こちらのStep.11で StringIndexOf という抽象操作を呼び出すことが記載されています。
抽象操作とは
言語仕様を準拠する上で処理系が実装すべき操作のことです。 公開していなくとも内部でこの振る舞いを実現する必要があります。 V8をはじめとする大体の処理系では、内部APIとして実装されています。
StringIndexOfのStep.2に 2. If searchValue is the empty String and fromIndex ≤ len, return fromIndex. と記載してあります。
また、String.prototype.includesのStep.11&12では NOT-FOUNDじゃなかったらtrueと記載されています。
これらのことから、空文字の場合は常に true が返ってくることが言語仕様として明記されていることが分かります。
なんで空文字を含む必要があるの?
ここで重要なことは、文字列は空文字を含むという命題は(ECMAScriptなどの)ローカルな取り決めではないということです。
形式言語における数学的な定義に基づいています。(らしいです)
https://ja.wikipedia.org/wiki/形式言語
ただし、長さゼロの空単語(Empty Word, 記号 e、ϵ、Λ)も含む。 チューリングマシンの言語は単なる文字列なので、数学的構造(他のチューリングマシンを含む)を扱うには符号化(エンコード)し、その数値を解釈するプログラムを埋め込む必要がある。 チューリング完全機械は十分強力なので、この手法であらゆる列挙可能な構造を扱うことができる。チューリングマシンの数値表現については(チューリングマシンの)表記(description)という。
(ちょっと何言ってるか分かんない)
ただし〜の文とその後の文には論理的なつながりはなさそうです。 単なる並列関係(共通のコンテキストを有しているだけ)かと思います。 ”Empty Word も含む” という数学的な定義をどこかの偉いオッサンが決めていそうだ、という情報が分かれば十分だと思います。
なんで空文字を含む必要があるの?ver.2
ふーん、決まりは分かったよ。 でも、どうしてその決まりになったのさ?
という疑問が湧きますよね。
結論からいうと、「シンプルになるから」です。
これだけだと分かりづらいので、詳しく見ていきましょう。
そもそも含むってなに?
「含む」という操作を、もっと基本的な操作で表現できないでしょうか?
文字列…含む…(そもそも文字列の操作は連結しか知らん…)
例えば "いろはにほへと" は "にほ" を含んでいますよね。
これを連結で言い換えるとどうなるか考えてみます。
“いろはにほへと" = なにか1 + "にほ" + なにか2 (連結を +, 等式を = とする)
こうしたとき「それぞれのなにかがともに存在すること」を含むの定義としてみます。
文字列が端っこのケース
それでは、”ちりぬるを” は "ちり" を含むといえるでしょうか?
先ほど作った定義に当てはめてみると
“ちりぬるを" = なにか1 + "ちり" + なにか2
となるような なにか1 を見つけなければなりません。
そこで…
なにか1 に空文字を割り当てて良いというルールにしてしまえば成立しそうですね!
このルールなら、条件分岐せずに先ほどの定義1つで含むを簡潔に表現できそうです。
空文字を含む理由
このルールを採用すると、文字列の両端には空文字 ε を含んでいそうです。
なぜなら、文字列はその文字列自身を含んでいそうだ、という直観を成立させる条件だからです。
単語の両端に含んでいるということは、それぞれの文字の間にも含んでいることになります。
なぜなら、文字列はそれを構成する1文字の連結で表現できるからです。
“いろは” = “い” + “ろ” + “は” = (ε + “い” + ε) + (ε + “ろ” + ε) + (ε + “は” + ε)
※ わかりやすさのために、着目している(文字列, 結合)の組み合わせに対して結合律が成立する前提でカッコを使用しています。
シンプルになるのはどうして?
一言でいえば、1つのルールで安全な操作を定義できるからです。
先ほどの例で言えば、文字列 = なにか1 + 部分文字列 + なにか2 となるようななにかに空文字OKとすることによって含むの定義をこれに一本化できたということです。
仮になかったとすれば、先頭一致・末尾一致・全体一致のケースは特別扱いし条件分岐が増えるでしょう。
ちょっと見方を変えると
今までの話は (文字列, 結合) ⇒ (整数, 積) と世界を変えてみると、「因数に必ず 1 を含むよね」というのと実は同じ話をしています。
先ほどの 空文字 や 1 のようなものを (集合, 演算) の組み合わせに対して”単位元”といいます。
単位元を持つような (集合, 演算) の組み合わせを”モノイド”といったりします。
単位元と似ている”反射律”という集合の順序が満たすと嬉しい性質もあったりします。
代数構造の公理で何でこんな意味不明な定義してるんだろうと思いがち(少なくとも私はそうでした)ですが、それらを満たすとロジックがシンプルになることが多いです。
コードとして関数を書くときだけではなく、システム全体を機能としてみたときに、このような公理的な性質をもつような操作かな?という確認や、そういう操作になるように定義しよう、と心がけていくと楽できるかもしれません😉