🎄

ENCA 3日目: プリミティブイテラブルを退ける規約制定

2024/12/03に公開

イテラブル(反復可能)とは

ES2015 から仕様の中にイテラブルインターフェースイテレーターインターフェースが定義されています。イテラブルインターフェースを実装したオブジェクトやプリミティブのことを単にイテラブル(反復可能)と呼びます。

ざっくり TypeScript の型で表現すると以下のようになります(実際の TypeScript での型はジェネリクスになっています)。

interface Iterable {
  [Symbol.iterator](): Iterator;
}

interface Iterator {
  next(value?: any): IteratorResult;
  return?(value?: any): IteratorResult;
  throw?(error?: any): IteratorResult;
}

interface IteratorResult {
  done: boolean;
  value: any;
}

イテラブルに対しては for...of や配列形式の分割代入などのシンタックスを使うことが出来ます。

const iterable = {
  [Symbol.iterator]() {
    let counter = 0;
    return {
      next() {
        if (counter >= 10) {
          return { done: true, value: undefined };
        }
        return { done: false, value: counter++ };
      },
    };
  },
}

for (const value of iterable) {
  console.log(value); // 0, 1, 2, ..., 9
}

詳しくは JSConf JP 2024 の sosukesuzuki さんの発表が参考になるかと思います。

https://www.youtube.com/live/2BXwigWGjWQ?si=8Ep2XVhdtTBxyr5Y&t=23349

https://speakerdeck.com/sosukesuzuki/iteretatoiteraburunogai-yao-toke-ti-wei-lai

プリミティブイテラブルについて

ECMAScript の文字列はプリミティブイテラブルです。String.prototype[Symbol.iterator] メソッドが定義されており、コードポイント単位で値を取り出すことが出来ます。

for (const char of "abcde") {
  console.log(char); // "a", "b", "c", "d", "e"
}

他のプリミティブに対してもプロトタイプ汚染をして Symbol.iterator メソッドを定義することで無理矢理イテラブルにすることが可能です(⚠️遊び以外で絶対にやらないでください)。

Number.prototype[Symbol.iterator] = function() {
  const limit = this;
  let counter = 0;
  return {
    next() {
      if (counter >= limit) {
        return { done: true, value: undefined };
      }
      return { done: false, value: counter++ };
    },
  };
};

for (const num of 10) {
  console.log(num); // 0, 1, 2, ..., 9
}

プリミティブイテラブルを許容したのは失敗だった

今日では ECMAScript でプリミティブイテラブルを許容したのは失敗だったと捉えられるようになりました。

文字列に対しては ES2022 Intl.Segmenter を使うと書記素クラスタや単語などの単位で分割することが出来ます。つまり今となってはコードポイント単位で分割するのが特に標準的な分割というわけではないのにも関わらず String.prototype[Symbol.iterator] メソッドではそのように分割されてしまっています。

const segmenter = new Intl.Segmenter("ja", { granularity: "grapheme" });

for (const { segment } of segmenter.segment("👍🏻👍🏼👍🏽👍🏾👍🏿")) {
  console.log(segment); // "👍🏻", "👍🏼", "👍🏽", "👍🏾", "👍🏿"
}

また文字列をコードポイントで分割して取り出したいとしても、文字列自身をイテラブルとして使うのではなく、それをラップしたイテラブルオブジェクトを作る方が扱いやすいです。ES2025 Iterator HelpersIterator.from を使ってオブジェクトイテラブルを作ること出来るため[1][2]、今後はこちらを使う方が良いでしょう。

for (const char of Iterator.from("abcde")) {
  console.log(char); // "a", "b", "c", "d", "e"
}

他のプリミティブに対してもプロトタイプ汚染しなければならないため、プリミティブをそのままイテレートするのは辞めたほうが良いです。

プリミティブイテラブルを退ける規約制定

プリミティブイテラブルの反省から、今後イテラブルを受け取る機能を追加する場合最初にオブジェクトかどうかのバリデーションを追加することが規約として[3]承認されました。

https://github.com/tc39/how-we-work/pull/152

新しい API が追加されるときには(Iterator.from のような明示的な変換機能以外は)原則としてオブジェクトイテラブルしか受け取れないようになります。

後方互換性の観点から、今すでにプリミティブイテラブルを許容している仕様に対して Normative Changes を入れることは叶いません。それらは全てドキュメントで列挙されています。列挙されているものは以下のとおりです。

  • 配列形式の分割代入
  • 配列や引数のスプレット構文
  • for...of
  • yield *
  • SetAggregateError コンストラクタ
  • Object.groupBy, Map.groupBy
  • Promise.all, Promise.allSettled, Promise.any, Promise.race
  • Array.from, %TypedArray%.from, Iterator.from

https://github.com/tc39/how-we-work/blob/main/normative-conventions.md#reject-primitives-in-iterable-taking-positions

関連する Web 標準仕様

Web IDL にて非同期イテラブルに変換する async iterable<T> が議論中で、オブジェクトのみを許容することになりそうです。

https://github.com/whatwg/webidl/pull/1397

この async iterable<T> は今後 ReadableStream.fromObservable.from で使われることが想定されます。なお Observable について記事があるので参考にしてください。

https://zenn.dev/pixiv/articles/471a8cf864d35f

さて、ReadableStream.from の提案を進めている Deno Land 社の Luca さんが Deno においてプリミティブを渡すと TypeError を投げる実装を入れました。それを見て、実行時だけでなく型でもエラーを出す PR を出して取り込んでもらいました

ただこれは変換機能なこともあって今後撤回されるかもしれません(正確には Web IDL で async iterable<any> or string 型になるかもしれない)。

Specifically the async iterable webidl type will not directly accept string primitives. However we might have decided something else for ReadableStream.from. I will have to check again.

https://github.com/WICG/observable/issues/125#issuecomment-2510232122

脚注
  1. Iterator.from 自身が String.prototype[Symbol.iterator] を呼び出しているだけではあるのですが。 ↩︎

  2. new String("abcde") のようなプリミティブラッパーオブジェクトを使う手もありますがおすすめしません。 ↩︎

  3. Normative Changes Advent Calendar 2024 の記事でありながら、Normative Changes ではなく Normative Conventions です……。ご容赦ください。 ↩︎

Discussion