ZhgChg.Li

AI 協作で資料構造RFC作成|多言語実装と技術設計の深掘り

技術者向けにAIを活用した資料構造RFCの作成過程を解説。問題抽象からアルゴリズム選定、Ruby・Swift実装まで、AIと共に進める技術研究の具体例を紹介します。

AI 協作で資料構造RFC作成|多言語実装と技術設計の深掘り
本記事は AI による翻訳です。お気づきの点があればお知らせください。

AIは単なるアプリ開発だけでなく:AIと協力してデータ構造RFCと多言語実装を完成させる

Rangeable RFC を例に、問題の抽象化、アルゴリズムの選択、意味の定義から Ruby / Swift の参考実装まで、AI がどのようにより深い技術研究と設計に関わったかを記録します。

背景

数週間前、個人サイト の再設計のためにClaude Code Maxを使い始め、その後AIとGoogle Apps Scriptを組み合わせて、古いiPhoneを活用した個人用デスクトップダッシュボードデッキを作りました。今週からは、以前作成したオープンソースプロジェクトに目標を切り替え、AIを使って手作業で書いたコードをリファクタリングし、効率化、ドキュメントの補充、テストの充実を図っています。

要約

まず AI を使っていくつかのオープンソースプロジェクトのアプリケーション層をリファクタリングし、それがきっかけでより深い Foundation のデータ構造設計に取り組むことになった。

Linkyee— あなた専用のリンクページ

完全カスタマイズ可能で、100%無料のオープンソースLinkTree代替品 — GitHub Pagesに直接デプロイ。

最初に改善したのは、以前作成した類似 Linktree の無料オープンソース自架版で、AIを使って構造を再構築し、いくつかのテーマを追加し、内蔵プラグインを増やし、ユーザーが簡単にカスタマイズできるDesign AIスキルを追加し、ローカルテスト環境も補強しました。

ZMediumToMarkdown

Mediumの投稿をクリーンなMarkdownとしてダウンロードし、構造、画像、リンク、コードブロック、一般的な埋め込みをプレーンなMarkdownやJekyllのワークフロー用に保持します。

あなたのために記事のすべての内容(画像、埋め込みコード、YouTubeリンクなど)をダウンロードし、Markdown形式に変換します。画像もすべてダウンロードして ./assets フォルダに保存します。

二つ目は、私が4年以上使い続け、メンテナンスしているプロジェクトです — 「Mediumの記事をダウンロードしてMarkdown形式に変換する」ツールで、後期はほぼMediumを記事編集のバックエンドとして使い、このツールで原文をダウンロード・変換してから私の自前のサイトにアップロードしています。

このプロジェクトは私のウェブサイトと Medium の重要な架け橋であり、途切れてはいけません。最初のバージョンを開発したときには多くの時間と労力を費やしましたし、ここ数年は細かい問題を修正し続けてきましたが、全体の設計を見直して最適化するのは久しぶりです。

同様にまずは AI による最適化応用から始め、手作業で書いたレンダリングロジックのリファクタリングとテスト補強を依頼しました。また、Medium の Cloudflare Anti-bot 対応も強化し、ユーザーが Medium のクローラー遮断に遭遇した際に、Chrome でログインしてクッキーを自動取得し、その後自動で処理を実行できるようにしました。

mcp-medium-reader

最後に AI は Medium.com 記事の Reader MCP サービスも構築しました。

主に基本的な変換サービスである ZMediumToMarkdown はすでに完成しており、MCP はそれを呼び出すラッパー層に過ぎません。

解決した問題は、ChatGPT、Codex、Claude、Claude Code などの AI に Medium の記事を渡すと、Medium の Cloudflare Anti-bot によってコンテンツのクロールがブロックされやすく、AI が記事を直接読み取れないことです。たとえ読み取れても、HTML を読み取るだけで、AI にとって扱いやすい Markdown 形式ではありません。

mcp-medium-reader AIが制限を突破し、Markdown形式のMedium記事を読み取り、トークンを節約しながらAIの理解力を向上させます。

ラベル付き区間集合の問題

当時 ZMediumToMarkdown を作っていたときに、この Foundation のデータ構造設計(アルゴリズム)問題 に直面しました。その時は余裕も能力もなく解決できませんでしたが、今は AI があるので、AI を使ってこの問題を解決してみようと思いました。

ケース1 — Mediumのレンダリング問題

Medium.com の GraphQL API が返す記事内容データは以下の形式です:

{
  "text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.",
  "markups": [
    {
      "type": "A",
      "start": 169,
      "end": 207,
      "href": "https://zhgchg.li/posts/f6713ba3fee3/",
      "anchorType": "LINK",
      "userId": null,
      "linkMetadata": null,
      "__typename": "Markup"
    },
    {
      "type": "STRONG",
      "start": 0,
      "end": 29,
      "href": null,
      "anchorType": null,
      "userId": null,
      "linkMetadata": null,
      "__typename": "Markup"
    },
    {
      "type": "EM",
      "start": 15,
      "end": 55,
      "href": null,
      "anchorType": null,
      "userId": null,
      "linkMetadata": null,
      "__typename": "Markup"
    },
    {
      "type": "STRONG",
      "start": 69,
      "end": 88,
      "href": null,
      "anchorType": null,
      "userId": null,
      "linkMetadata": null,
      "__typename": "Markup"
    }
  ]
}

わかりやすく翻訳:

[0,14]    STRONG       = Lorem ipsum dol
[15,29]   STRONG + EM  = or sit amet, co
[30,55]   EM           = nsectetur adipiscing elit,
[56,68]   normal       =  sed do eiusm
[69,88]   STRONG       = od tempor incididunt
[89,168]  normal       =  ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercit
[169,207] A            = ation ullamco laboris nisi ut aliquip e

期待されるレンダリング結果:

注意:Medium の元データの end の意味は実際の API 定義に従って変換する必要があります;本文では以降、Rangeable RFC の閉区間の意味で説明します。

問題

  • 区間の重ね合わせ
    同じ区間に複数の状態が重なることがあります。例えば、[15,29] は STRONG + EM です。

  • 区間の交錯
    極端な場合、ユーザーが交錯するスタイルを設定することがあります。例えば、[0,29] は STRONG、[15,55] は EM です。

  • 区間の連続マージ
    データが [0,12] は STRONG / [12,29] は STRONG の場合、[0,29] は STRONG にマージする必要があります。

区間の重なりや連続した区間の結合は比較的処理しやすいですが、一番厄介なのは区間が交錯して閉じる場合の処理です。インデックス通りにレンダリングすると **AA_AA**BBB_ のようになり、正しくは開閉を調整して **AA_AA_**_BBB_ としなければなりません。

iOS 開発者で NSAttributedString attributes を使ったことがある方も同じ問題に直面しますが、Apple Foundation は区間のマージや重複処理をうまく処理してくれています。

当時もかなり長い時間考え込んでしまい、本が役に立たないと痛感しました。普段あまり問題演習をしていなかったため、実際に使う場面で武器がありませんでした。その時は最も力技のウォークスルーで解決しましたが、正確ではあったものの性能とコードの質が非常に悪かったです。

関連する LeetCode の問題タイプ

  • 区間のマージ
56. 区間のマージ
57. 区間の挿入

用途: STRONG [0,12] と STRONG [13,29] を結合して STRONG [0,29] にする。

  • スイープライン
253. 会議室 II
731. マイカレンダー II
732. マイカレンダー III
1094. カープーリング
1851. 各クエリを含む最小間隔

用途:

STRONG [0,29]
EM     [15,55]
A      [169,207]

〜へ

0    open STRONG
15   open EM
30   close STRONG
56   close EM
169  open A
208  close A
  • 差分配列(Difference Array)ですが、純粋な数値の差分ではありません
370. Range Addition
1109. Corporate Flight Bookings
1094. Car Pooling

用途: アクティブなマークアップセットの管理

  • 区間分割 / セグメント構築

用途:

STRONG [0,29]
EM     [15,55]

〜へ

[0,14]   STRONG
[15,29]  STRONG + EM
[30,55]  EM

他に使われた問題タイプ、データ構造:

  • イベントのソート

  • 区間分割 / セグメント分割

  • スタック / 括弧のマッチング

  • 二分探索

  • Ordered Set / LinkedHashSet

  • 正規化

ケース2 — AVPlayer キャッシュ区間の問題

上記の Medium での区間問題は、実は私も以前似たような問題に直面しました。開発中の「AVPlayer ローカルで再生しながらキャッシュする時」でも同様の課題がありました。

ストリーミングデータは連続していないため、サイズが1000のデータに対して、AVPlayerは [0,100] [300–500] [150, 200]… のような連続していない、または交差するデータを要求することがあります。

以上の図を例に、現在のデータ区間: [250,566] [850,959] と仮定すると、理想的には:

  • AVPlayer に [0,300] を問い合わせた時: [0,249] はリモートから取得、[250,300] はローカルから取得

  • AVPlayer が [350, 920] を問い合わせたとき: [350,566] はローカルから取得、[567,849] はリモートから取得、[850,920] はローカルから取得

当時も同様に区間計算の問題に直面しました — Covered / Uncovered Interval Query、ただし Medium の問題より簡単で、こちらはバイナリデータの0と1の区別だけで、結果の計算だけが必要でした。

*当時も一時的に解決できませんでした。なぜなら、AVPlayer の再生しながらキャッシュする機能の開発にほとんどの時間を費やしたからです。また、その時のシナリオは音声で、ファイル自体が小さかったため、区間にデータがなければ全て取得して上書きする方法を先に採用しました — 詳細は元記事「 AVPlayer 本地 Cache 実装攻略|使用 AVAssetResourceLoaderDelegate で iOS 音楽ストリーミングの通信量を節約 」をご参照ください。 *

全体的な協力プロセス

AIを使ったFoundationデータ構造設計の実装

ツール

  • Claude Code Max

  • 努力: 最大限度

  • モデル: Opus 4.7

フロー

  • まずは AI Plan Mode を使って RFC 作成計画を立てる

  • AIに研究を開始させ、2つのエージェント役割を設定してください:一人はRFCの研究と執筆を担当する大学院生、もう一人はアルゴリズムの効率性と妥当性のレビューに専念する教授(レビューは承認されるまで継続)。

  • 研究生はまず Medium のケースを実験として、Ruby で開発してみることができます。

  • 最終生成された RFC.md ファイル

  • 他のエージェントに各言語での実装を任せる(私は ZMediumToMarkdown は Ruby、AVPlayer は iOS Swift の問題)

1. AIによるデータ構造RFCの検討 — Rangeable RFC

プロンプト(計画モード):

/plan

英語を使用し、英語の論文研究を参考にしてRFC実装ドキュメントを作成してください。後で他の言語がこのドキュメントに従って実装できるようにします。作成期間中はRubyを実験言語として使用しても構いません。
すべてのデータ構造とアルゴリズムを研究し、時間と空間効率のバランスが最も良いアルゴリズムを見つけて要件を達成してください。
ツール名:Rangeable
機能:ジェネリックオブジェクトの連続および非連続集合を計算する。
例:
var strings:Rangeable<String> = []
strings.insert(Strong(), start: 2, end: 5) // 2-5 Strong
strings.insert(Strong(), start: 3, end: 7) // 3-7 Strong
strings.insert(Strong(), start: 9, end: 11) // 9-11 Strong
strings.insert(Italic(), start: 3, end: 8 ) // 3-8 Italic

// 使用例:
Strong().getRange(from: strings) -> [{2,7},{9-11}]
Italic().getRange(from strings) -> [{3,8}]
//
strings[4].objs -> [Strong(), Italic()]
strings[8].objs -> [Italic()]
strings[10].objs -> [Strong()]
//
Strong, Italic はそれ自体もジェネリックですが、ここでは例として挙げています
//

実際にRFCを作成する際は、サブエージェントを使い、研究生が実装方法の研究とRFCの執筆を担当し、教授が学術的またはより深いアルゴリズムの観点から研究生エージェントの成果をレビューします。教授エージェントに却下された場合は再度研究してRFCを書き直してください。
まずは ./RubyRangeable にRuby実装、./SwiftRangeable にSwift実装を作成してください。
トークンやリソースを惜しまず研究に使って構いません。
//
実際の適用例:../ZMediumToMarkdown 内の MarkupStyleRender.rb
現在はMedium GraphQLが返すParagraphsを解析しています
{"id": "f30dc1c4fe6c_19", "name": "b2ba", "type": "BQ", "href": null, "layout": null, "metadata": null, "text": "A notification webhook is an endpoint you create on your server.\n通知型 Webhook は自分のサーバー上に作成するエンドポイントです。", "hasDropCap": null, "dropCapImage": null, "markups": [{"type": "EM", "start": 0, "end": 104, "href": null, "anchorType": null, "userId": null, "linkMetadata": null, "__typename": "Markup"}], "__typename": "Paragraph", "codeBlockMetadata": null, "iframe": null, "mixtapeMetadata": null},
{"id": "f30dc1c4fe6c_20", "name": "f2e8", "type": "BQ", "href": null, "layout": null, "metadata": null, "text": "This webhook endpoint receives HTTP POST requests from App Store Connect.\nこのWebhookエンドポイントはApp Store ConnectからのHTTP POSTリクエストを受け取ります。", "hasDropCap": null, "dropCapImage": null, "markups": [{"type": "EM", "start": 0, "end": 126, "href": null, "anchorType": null, "userId": null, "linkMetadata": null, "__typename": "Markup"}], "__typename": "Paragraph", "codeBlockMetadata": null, "iframe": null, "mixtapeMetadata": null},
{"id": "f30dc1c4fe6c_21", "name": "1725", "type": "BQ", "href": null, "layout": null, "metadata": null, "text": "The POST requests describe important events about your app.\nこれらのPOSTリクエストはあなたのアプリに関する重要なイベントを説明します。", "hasDropCap": null, "dropCapImage": null, "markups": [{"type": "EM", "start": 0, "end": 89, "href": null, "anchorType": null, "userId": null, "linkMetadata": null, "__typename": "Markup"}], "__typename": "Paragraph", "codeBlockMetadata": null, "iframe": null, "mixtapeMetadata": null},
{"id": "f30dc1c4fe6c_22", "name": "3e9d", "type": "BQ", "href": null, "layout": null, "metadata": null, "text": "Use the webhooks notifications endpoint to configure the notifications for events happening to your apps.\nWebhook通知エンドポイントを使って、アプリで発生するイベントの通知設定ができます。", "hasDropCap": null, "dropCapImage": null, "markups": [{"type": "EM", "start": 0, "end": 151, "href": null, "anchorType": null, "userId": null, "linkMetadata": null, "__typename": "Markup"}], "__typename": "Paragraph", "codeBlockMetadata": null, "iframe": null, "mixtapeMetadata": null},
そしてstart, endに従ってMarkdownをレンダリングしますが、現在はアルゴリズムがないためループで埋める方法を使っています。

2. 方案確定後研究生 Agent 開始研究 -> 初版RFCを作成開始

3. 教授エージェントがレビュー開始 -> REJECTED

主な理由は、教授が RFC にまだ 6つの必須修正事項(MUST-FIX) があると考えており、これらは修正しなければ通過できない仕様/アルゴリズムの正確性の問題だからです。

4. 研究生エージェントが再調査・修正 → 第二版RFCを作成

5. 教授エージェントによる再レビュー -> 承認済み

6. 完了

  • 所要時間:約 1 時間 30 分

  • 消費したトークン:178K(Claude Code Max 5時間利用可能枠の約30%)

Rangeable RFC

TL;DR

Rangeable<Element> は「要素がどの整数閉区間内で有効か」を管理する汎用データ構造です。同じ要素の重複または隣接する区間を自動的にマージし、特定のインデックスでどの要素がアクティブかを検索したり、ある範囲内のオープン/クローズのトランジションイベントを出力したりできます。元々は Medium Markdown のマークアップレンダリング問題、例えばある文字に STRONGEMLINK のどのスタイルが同時に適用されているかを判定するために作られましたが、同様に カレンダー、ゲーム状態、ゲノムアノテーション、AVPlayer のバイトレンジキャッシュなどの場面にも適用可能です。コアバリューは「区間のマージ、アクティブセットの検索、境界イベントの生成」を決定的でクロスランゲージに一貫した仕様に抽象化し、Ruby や Swift での実装に適した形にしたことです。

以下の RFC の詳細は、特に興味がなければ飛ばしても構いません。ここも AI による要約翻訳を使っています。

Rangeable<Element> は「要素を複数の整数閉区間に対応させ、ある位置でどの要素が有効かを高速に検索できる」汎用データ構造です。

それが解決するのは単純な Range の問題ではなく、多くの (要素, start, end) を与えられたときに、以下を実現できることです:

  1. ある要素が含まれる結合後の区間を調べる

  2. あるインデックスでどの要素が有効かを調べる

  3. ある範囲内にどのような open / close の境界イベントがあるか調べる

RFC は Rangeable を言語に依存しない、ジェネリックで整数座標の閉区間セットコンテナとして明確に定義しています。要素はハッシュ可能で、比較は値の等価性に基づきます。getRanger[i].objstransitions の3種類のクエリをサポートしています。

主な動機

この RFC は ZMediumToMarkdown の Medium マークアップレンダリング問題に起因しています。

Medium の段落内にはこのようなマークアップがあります:

STRONG: [2, 5]
STRONG: [3, 7]
EM:     [4, 10]

現在のレンダリングでは、各文字ごとにどのタグがアクティブか(例えば bold、italic、link、code など)を逐一スキャンして判定しています。RFC では、従来の方法が markups を TagChar に変換し、startIndex によってソートした後、各文字に対して全てのタグを線形に走査するため、最悪で O(L · m) になると述べています。Rangeable はこれを汎用コンテナとして抽象化し、レンダラーは単に markups を挿入し、あとは r[i].objstransitions を使ってクエリするだけで済むようにしたいと考えています。

RFC はこの問題を他のシーンにも拡張しています。例えば:

RFC は共通の問題として次のことを明確に指摘しています:多くの (eᵢ, lᵢ, hᵢ) が与えられたとき、ある位置 i を検索すると、l ≤ i ≤ h を満たすすべての要素を返す必要があります。

コアコンセプト翻訳

1. Rangeable<Element>

こう考えてもいいです:

Element -> [ClosedRange<Int>]

しかし、それは普通の Dictionary ではありません。なぜなら、次のことを行うからです:

  1. 同じ要素の重複する区間を自動的にマージする

  2. 隣接する区間の自動マージ

  3. 要素の最初の挿入順序を保持する

  4. あるインデックス上でアクティブな要素を高速に検索するサポート

  5. オープン/クローズのトランジションイベントの出力をサポート

例えば:

insert(STRONG, 2, 5)
insert(STRONG, 3, 7)

最後はこうなります:

STRONG -> [(2, 7)]

なぜなら [2,5][3,7] が重なっているからです。

APIのポイント

RFC 定義の主な API は以下の通りです。

insert(e, start, end)

insert(e: Element, start: Int, end: Int)

効果は:

R(e) = canonicalize(R(e)  [start, end])

つまり、新しい区間を要素 e の既存の区間集合に追加し、canonicalize を行う:重複または隣接するすべての区間をマージしてソートする。start > end の場合は InvalidIntervalError を投げる必要がある;同じ内容を重複して挿入することは冪等であり、結果を変更せず、バージョンも増やしてはいけない。

r[i].objs / activeAt(index:)

r[i] -> スロット
r.activeAt(index: i) -> スロット

ある index 上でアクティブな要素を返す。

例えば:

insert(Strong, 2, 5)
insert(Italic, 3, 7)

検索:

r[3].objs

結果は:

[Strong, Italic]

クエリの複雑度の目標は:

O(log M + r)

ここで M はすべての要素をマージした後のインターバルの総数、r はその位置で実際に返される要素の数です。

getRange(of:)

getRange(of e) -> [(Int, Int)]

ある要素の現在のマージ後の正規化範囲を返す。

例えば:

insert(Strong, 2, 4)
insert(Strong, 5, 7)

なぜなら [2,4][5,7] は整数座標上で隣接しているため、結果は:

[(2, 7)]

RFC は、返される intervals が必ずソートされており、重複せず、隣接していないことを明確に要求しています。

transitions(over:)

transitions(over: ClosedRange<Int>) -> [TransitionEvent]

範囲内の open / close 境界イベントを返します。

例えば:

insert(Strong, 2, 5)
insert(Italic, 3, 7)

すると transitions は以下のようになります:

[
  (2, open,  Strong),  -- Strongタグを開く
  (3, open,  Italic),  -- Italicタグを開く
  (6, close, Strong),  -- Strongタグを閉じる
  (8, close, Italic)   -- Italicタグを閉じる
]

注意すべきは close イベントの位置が hi + 1 であることです。外部の意味としては閉区間 [lo, hi] ですが、sweep-line 内部では「最初にアクティブでなくなる位置」を close 座標として使用します。RFC では transitions(over: lo..hi)hi + 1 の close イベントを含むと明確に記載されており、右端の処理を容易にしています。

区間の意味

1. end は包含されます

この RFC は非常に明確です:insert(e, start: a, end: b) は閉区間 [a, b] を表し、b 自体もアクティブな範囲に含まれます。

つまり:

insert(Strong, 2, 5)

代表:

2, 3, 4, 5 はすべてアクティブ

[2, 5) ではありません。

RFC が inclusive を選択した理由は以下の通りです:

  1. Medium マークアップ / ZMediumToMarkdown に適した履歴データモデル

  2. 「この文字がアクティブかどうか」という人間の直感に合っている

  3. 整数の隣接マージは hi + 1 == lo と表現できます。

  4. 単一区間 [k, k] は有効な位置を自然に表現できます

2. 隣接する区間は結合する

整数座標上で:

[2, 4] + [5, 7] => [2, 7]

4 と 5 の間に「非アクティブ」を表す整数位置がないため、RFC では同じ要素の隣接する整数区間を必ず結合する必要があります。

しかし:

[2, 4] + [6, 7] => [(2, 4), (6, 7)]

中間に 5 のギャップがあるためです。

3. start == end は有効なシングルトン区間です

insert(e, 5, 5)

代表インデックス 5 のみがアクティブです。RFCではこれが有効かつ空でない区間であることを要求しています。

4. start > end は必ずエラーを投げる必要がある

insert(e, 10, 5)

自動的に [5,10] に反転することも、サイレントに正規化することもできません。RFC では InvalidIntervalError を必ずスローし、かつコンテナの状態を変更してはいけません。

Element equality の意味

Rangeable は、2つの要素が同じ要素かどうかを判定する際に、言語ネイティブの値の等価性を使用します。

Ruby:

eql? + hash

Swift:

Hashable / Equatable

なので:

Link("a")  Link("a") は同じ要素と見なされ、区間が結合されます
Link("a")  Link("b") は異なる要素であり、結合されません
Strong()  Strong()  equality が等しい場合、結合されます

RFC は、等価性が反射律、対称律、推移律、およびハッシュの一貫性を満たす必要があり、要素が挿入された後に外部からの変更によってハッシュや等価性が破壊されてはならないと要求しています。

ソートルール

これは RFC の非常に重要な部分です。

アクティブセットのソート

r[i].objs の順序はハッシュでもアルファベット順でもレンジの長さでもなく、以下の通りです:

-- 要素が初めて挿入された順序

例えば:

insert(Strong, 1, 10)
insert(Italic, 1, 10)
insert(Code, 1, 10)

則:

r[5].objs == [Strong, Italic, Code]

RFC は、これを行う理由として、決定論的で言語間の一貫性があり、Markdown のネストに適合していることを挙げています:早く出現するスタイルほど通常は外側になります。

マージは挿入順序を変更しません

例えば:

insert(Strong, 1, 5)   // Strong の順序 = 1
insert(Italic, 3, 7)   // Italic の順序 = 2
insert(Strong, 4, 8)   // Strong の範囲は [1,8] にマージされますが、順序は 1 のままです

検索:

r[6].objs

結果は:

[Strong, Italic]

Strong は3番目に index 6 まで拡張されましたが、その first-insert order は Italic よりも早いです。RFC はこのケースで「要素が初めて現れた順序」こそがソートの基準であることを明確にしています。

Transitions の並び替え

同じ座標上で:

  1. .open.close より先に来ます

  2. 複数の .open :挿入順で昇順

  3. 複数の .close :挿入順で降順、つまり LIFO

このようにすれば Markdown のスタック規律に適合します:

** 開く
_ 開く
_ 閉じる
** 閉じる

RFC は、このタイブレークルールを明確に定義しており、Ruby と Swift のハッシュ順序の不一致による言語間の出力差異を防いでいます。

内部データ構造

RFC のコア設計の選択は次のとおりです:

Map<Element, SortedList<Interval>>
+ 挿入順序
+ 遅延境界イベントインデックス
+ バージョンカウンター

1. 要素ごとのソートされた非重複リスト

各要素にはそれぞれ独自のインターバルリストがあります:

intervals: Map<Element, SortedList<Interval>>

そして、canonical form を維持しなければなりません:

  1. lo による昇順ソート

  2. お互いに重ならない

  3. 隣接していない

  4. 各区間は常に lo <= hi を満たす

RFC ではこれを I1 不変条件と呼びます。

2. レイジー境界イベントインデックス

Rangeable は毎回 insert のたびに検索インデックスを再構築するのではなく、最初の query 時に初めて構築します。

version: Int
event_index: EventIndex?

event_index には以下が含まれます:

events: SortedArray<Event>
segments: SortedArray<Segment>
version: Int

この設計は「一度構築してから密にクエリを行う」ワークロードに対応するためのものです:大量の挿入が完了した後、各位置を集中的に検索します。RFCでは、ビルドフェーズでインデックスを何度も再構築する必要がないため、lazy indexがこのパターンに適していると明確に述べています。クエリフェーズでは一度だけ O(M log M) のコストがかかります。

アルゴリズムのポイント

insert のコアプロセス

RFC の擬似コードは大まかに以下の通りです:

function insert(r, e, start, end):
  if start > end:
    raise InvalidIntervalError

  e_frozen = freeze_for_insert(e)

  if e not in intervals:
    intervals[e] = []
    insertion_order.append(e)
    ord[e] = insertion_order.length

  list = intervals[e]
  lo = start
  hi = end

  # [lo, hi] と重複または隣接する最初の interval を見つける

  while interval が [lo, hi] と重複または隣接する:
    lo = min(lo, interval.lo)
    hi = max(hi, interval.hi)
    # 古い interval を削除する

  # マージした新しい [lo, hi] を挿入する

  if 実際に変更があった場合:
    version += 1
    event_index = nil

特に RFC では、lo - 1 を使って判定してはいけないと注意しています。lo == Int.min の場合にアンダーフローが発生するためです。代わりに hi + 1 >= lo または同等の successor モデルを使うべきです。

複雑さ

RFC が選択した参考構造は:

要素ごとにソトされた非重複リスト + 遅延イベントインデックス

複雑度はおおよそ以下の通りです:

その中で:

m_e = ある element 自身のマージされた区間の数
k = 今回の挿入で吸収/合併された古い区間の数
M = すべての element のマージされた区間の総数
r = クエリ位置で実際にアクティブな element の数

RFC はまた、同じ位置に実際に m 個の要素がアクティブであれば、出力自体が Ω(m) を必要とするため、O(log M + m) はすでに出力感度のある合理的な境界であると指摘しています。

なぜ Interval Tree / Segment Tree / Roaring Bitmap を使わないのか?

RFC には代替案を比較したセクションがあり、最終的に (a) Per-element sorted disjoint list + lazy event index を選択しました。

重要なポイントは以下の通りです:

RFC 総括でメイン構造を選んだ3つの理由:

  1. per-element merge の意味が自然で、getRange は直接 R(e) を返すことができます

  2. 決定論的で、クロスランゲージで再現可能です

  3. lazy index は一度構築してからクエリするワークロードに非常に適しています。

v1 に含まれていない機能

RFC はっきりと v1 で行わないことを列挙:

remove(e, start, end)
remove(e)
clear
union / intersect / difference
persistent immutable snapshot
浮動小数点座標
多次元矩形スタッキング
r[lo...hi].objs のような range slot query

削除や交差部分もAIに任せて、RFC V2で実装してください。

AIによりRangeable RFCを基に言語実装を行う

フロー

  1. 同様にまず /plan の Plan Mode を実行し、AI に RFC を詳細に読み込ませて実装を計画させる

  2. 同様に developer と reviewer に分けて、一方が作成しもう一方がレビューを行い、問題がなくなるまで繰り返します。

  3. Readme 使用ドキュメントの作成と GitHub へのプッシュ

最も難しい RFC 仕様の研究が完了した後は、AI がこの仕様に従って実装するだけで、ほとんど問題が発生しません。

現在実装されている言語は以下の通りです。

実際に AI を活用して設計・開発した — Rangeable Foundation

すべて準備が整ったら、最初のオープンソースプロジェクト ZMediumToMarkdown のアルゴリズム問題に戻り、AI にこのライブラリを適用して、元の複雑なウォークスルーの方法を取り除き、構造を最適化+ 適用前後のテスト検証を追加 してください。

ZMediumToMarkdown 3.6.0

パフォーマンス

  • マイクロベンチマーク:マークアップレンダリングのホットパスで5.5倍高速化。

  • 実際の Medium 記事でのエンドツーエンド処理:2.23倍高速化
    (平均で1段落あたり2073 µs → 930 µs)。

AVPlayer ローカルキャッシュ実装攻略

記事内容には SwiftRangeable の適用例も追加されています。

AIに感嘆する

これまでは AI を使って ウェブサイトの再設計個人ダッシュボード 、あるいは仕事上の製品問題の修正などの応用的な問題に取り組んでいましたが、初めてアルゴリズムや基盤データ設計の問題を深く研究し解決するよう依頼しました。

効果は私の想像を超えました。彼が書いたRFCをざっと読んでみましたが、形式も内容も実際の人間が研究して書いたものに劣らず(少なくとも私が書いたものよりは確実に良い)、途中で彼が何をしているか観察すると、Web検索を使って公開されている関連論文や技術文献を調べ、それらを統合して実装の妥当性を評価していました。doer / reviewer に役割を分けた効果も非常に顕著で、doerは実装に偏りがちですが、reviewerはより広く高い視点からdoerの作業に副作用がないか俯瞰的に見ています。最後にRFCが決まった後、AIに基づいて実装を依頼すると、その正確性はほぼ100%でした。

AI 使用の振り返り

ただし、皆さんはAIに取って代わられることを恐れる必要はありません。問題を解決する思考こそが重要であり、このAIはそこまで強くありません。例えば、直接AIにZMediumToMarkdownの最適化を依頼しても、Mediumのシーンや元のコードの書き方にしか集中できないでしょう。しかし、人間はFoundationとして抽象化し、まずRFCを研究し、最終的に実装することで、より良い結果を得られることを知っています。もちろん、自分たちで行うことも可能ですが時間がかかります。AIはそのプロセスを加速するものであり、取って代わるものではありません。

Post Mediumから変換されたもの by ZMediumToMarkdown.

GitHub で編集
この記事を改善
本記事は Medium で初公開
オリジナルを読む
この記事をシェア
リンクをコピー · SNS でシェア
ZhgChgLi
著者

ZhgChgLi

An iOS, web, and automation developer from Taiwan 🇹🇼 who also loves sharing, traveling, and writing.

コメント