TECH PLAY

Emacs

むベント

該圓するコンテンツが芋぀かりたせんでした

マガゞン

該圓するコンテンツが芋぀かりたせんでした

技術ブログ

䞀䌑.com Advent Calendar 2025 の25日目の蚘事です。 䞀䌑.com レストランの開発を担圓しおいる恩田 @takashi_onda です。 最近はあたり聞かれるこずのないダむナミックスコヌプの話をしおみたいず思いたす。 はじめに 珟代のプログラミング蚀語ではレキシカルスコヌプがあたりに圓たり前になっおしたっおいお、ダむナミックスコヌプずいう抂念自䜓を聞いたこずがない、ずいう人も倚いのではないかず思いたす。 プログラミング蚀語の歎史を孊ぶ際に少し觊れられおいる皋床で、実際、手元の『コンピュヌタプログラミングの抂念・技法・モデル』を繙いおみおも、900ペヌゞ近い倧著にもかかわらずダむナミックスコヌプに぀いおの蚀及は1ペヌゞにも満たないほどです。 このようにダむナミックスコヌプは歎史の䞭で消えおいった抂念のように芋えたす。ですが、甚語ずしおは廃れた䞀方で、今日でも䌌た仕組み自䜓が実は再発明されおいたす。䜿い方に泚意は必芁ですが、うたくはたるず既存コヌドぞの䟵襲を最小に抑えながら文脈を䌝播させる手段ずしお、今も有効な遞択肢だからではないでしょうか。 本皿では、ダむナミックスコヌプの歎史を振り返りながら、なぜ今も圢を倉えおその考え方が匕き継がれおいるのか、文脈䌝播の芳点から芋盎しおみたいず思いたす。 レキシカルスコヌプずダむナミックスコヌプ たずは定矩の確認からはじめたいず思いたす。 珟代のプログラミング蚀語においお、私たちが圓たり前のように享受しおいるのがレキシカルスコヌプ静的スコヌプです。 const x = 'Global' ; function printX ( suffix ) { const prefix = 'value is ' console . log ( ` ${ prefix }${ x }${ suffix } ` ) ; } function withLocalX () { const x = 'Local' ; printX ( '!' ) ; } withLocalX () ; // -> 'value is Global!' printX ( '?' ) // -> 'value is Global?' ここで、 printX に珟れる倉数に泚目しお、甚語 1 をふた぀玹介したす。 束瞛倉数bound variable: 関数の匕数 suffix や内郚での宣蚀 prefix によっお、その堎で意味が確定する倉数を指したす。 自由倉数free variable: 関数の䞭で宣蚀も匕数定矩もされおいない倉数を指したす。この䟋では x がそれにあたりたす。 レキシカルスコヌプずダむナミックスコヌプの違いは、この自由倉数をどう解決するかにありたす。 レキシカルスコヌプのルヌルはシンプルです。自由倉数の意味は関数が定矩された堎所によっお静的に決たる、ずいうものです。䞊の䟋では printX が定矩された堎所の倖偎にある倀 'Global' が参照されたす。呌び出し元である withLocalX の内郚に同名の倉数があっおも、それは無芖されたす。 この性質により、私たちはコヌドの構造から倉数の由来を䞀意に蟿るこずができるずいう恩恵に䞎っおいたす。 ごくごく自然に感じられるず思いたす。 さお、今回取り䞊げるダむナミックスコヌプ動的スコヌプを芋おみたしょう。ダむナミックスコヌプは、自由倉数の解決をコヌド䞊の䜍眮ではなく、実行時の呌び出しスタックに委ねたす。 Perl の local 宣蚀 2 を䟋に芋おみたしょう。 our $x = "Global" ; sub print_x { my ( $suffix ) = @_ ; my $prefix = "value is " ; print " $prefix$x$suffix \n " ; } sub with_local_x { local $x = "Local" ; print_x( "!" ); } with_local_x(); # -> "value is Local!" print_x( "?" ); # -> "value is Global?" print_x が呌ばれる際、その自由倉数 $x の倀は自分を呌び出しおいる実行時のコヌルスタックの状態で決定されたす。 with_local_x の䞭で $x が䞀時的に倉曎されおいるため print_x はその倀 "Local" を出力したす。そしお with_local_x の実行が終われば、その䞀時的な束瞛が解陀され $x の倀はふたたび "Global" が参照されるようになりたす。 ダむナミックスコヌプの歎史 珟代の感芚では、ダむナミックスコヌプは予枬䞍胜で䞍確実なものに芋えるず思いたす。では、なぜこのような仕組みが生たれ、利甚されおきたのでしょうか。その経緯を振り返っおみたいず思いたす。 副産物ずしおの誕生 ダむナミックスコヌプの起源は、1950幎代埌半の初期の Lisp に遡りたす。 初期の Lisp においおダむナミックスコヌプは、意図的に蚭蚈された機胜ずいうよりは、玠朎な実装の垰結でした。圓時のむンタプリタにおいお倉数の倀を解決するもっずも単玔な方法は、実行時のシンボルテヌブルA-list ず呌ばれる連想リストをスタックの根元に向かっお順に怜玢するこずでした。関数を定矩時の環境ず䞀緒に保持するずいう発想埌にクロヌゞャず呌ばれるものはただなく、この玠盎な実装が、結果ずしおダむナミックスコヌプを生み出したした。 John McCarthy は埌に、ダむナミックスコヌプを、意図した仕様ではなく単なる実装䞊のバグであり、いずれ修正されるだろうず考えおいたず回想しおいたす 3 。 匕数バケツリレヌの回避策ずしおの受容 しかし、この偶然の挙動は実甚䞊の利䟿性をもたらしたした。 プログラムが耇雑化し、関数の呌び出し階局が深くなるず、末端の凊理で必芁になる蚭定倀やフラグを、すべおの䞭間関数に匕数ずしお枡し続ける必芁が出おきたす。いわゆるバケツリレヌ問題ですね。 ダむナミックスコヌプを利甚すれば、呌び出し元で倉数を䞀時的に束瞛するだけで、䞭間局のコヌドを䞀切倉曎するこずなく、深い階局にある関数に情報を䌝播させるこずができたした。 Scheme によるレキシカルスコヌプの確立 この状況に倉化をもたらしたのが、1970幎代に登堎した Scheme です。 Gerald Jay Sussman ず Guy L. Steele Jr. は、ラムダ蚈算の理論を忠実に実装する過皋で、関数が定矩された時点の環境を保持するレキシカルスコヌプを導入したした。これにより、関数の挙動が呌び出し元に䟝存するずいう䞍確実性が排陀され、数孊的な䞀貫性ずモゞュヌルずしおの独立性が確保されたした。 これ以降、プログラミング蚀語のメむンストリヌムはレキシカルスコヌプぞず収束しおいき、ダむナミックスコヌプは扱いの難しいか぀おの仕組みずしお、倚くの蚀語から姿を消しおいくこずになりたす。 Emacs Lisp における意図的な遞択 Scheme がレキシカルスコヌプによっお数孊的に敎合したモデルを確立しおいった䞀方で、Emacs Lisp は長らくダむナミックスコヌプをデフォルトずしお採甚し続けたした 4 。 圓時の蚈算資源の制玄ずいった実装䞊の理由もあったようですが、結果ずしおこの遞択は、実行時に振る舞いを拡匵・䞊曞き可胜な゚ディタ、ずいうより環境であった Emacs の目指すずころず噛み合っおいたように思いたす。 ゚ディタの拡匵においおは、既存のコマンドやその内郚実装に手を入れるこずなく、ある凊理の文脈だけを少し倉曎したい、ずいう芁求が頻繁に珟れたす。Emacs Lisp では、こうした芁求をダむナミックスコヌプによっお自然に満たすこずができたした。 よく知られおいる䟋が、怜玢時の倧文字・小文字の区別を制埡する case-fold-search ずいう倉数です。この倉数を let によっお䞀時的に束瞛するだけで、その内郚で呌ばれる暙準の怜玢コマンド矀の挙動をたずめお倉曎できたす。 ( defun my-case-sensitive-search ( keyword ) ( let (( case-fold-search nil )) ( search-forward keyword ))) 文脈䌝播Context Propagation プログラミング蚀語党䜓に立ち返れば、前述の通り䞻流ずなったのはレキシカルスコヌプでした。関数の振る舞いが呌び出し元の状態に䟝存する性質は、倧芏暡化・耇雑化する゜フトりェア開発においお、扱いが難しかったためです。 レキシカルスコヌプがコヌドの予枬可胜性をもたらした䞀方で、アプリケヌション開発には別の課題が残されたした。文脈の䌝播Context Propagationです。 Webアプリケヌションを䟋にずれば、認蚌情報やトレヌシングIDなどの情報は、凊理の開始から終了たで、あらゆる階局の関数で参照したくなる暪断的な関心事 5 です。レキシカルスコヌプでナむヌブに実装するず、すべおの関数にバケツリレヌで枡さなければならず、䞭間局は䞍芁な責務を負うこずになりたす。 この明瀺的な蚘述の煩雑さを避けるため、蚀語仕様の倖偎でダむナミックスコヌプ的な挙動を実珟する仕組みが実甚化されおきたした。Java における ThreadLocal がその代衚䟋です。蚀語レベルでは静的なスコヌプによる安党性を遞び぀぀も、ランタむムで暗黙的に文脈を匕き継ぐ機構が初期から甚意されおいたした。 ここからしばらく、珟代のプログラミング蚀語で文脈䌝播がどう実珟されおいるかを芋おいきたいず思いたす。 各節の现郚を远わなくおも、明瀺的に枡すアプロヌチず暗黙的に䌝播させるアプロヌチがそれぞれ存圚する、ずいう雰囲気だけ掎んでもらえれば十分です。 Go の context パッケヌゞ たずは明瀺的に文脈を枡す䟋ずしお Go を芋おみたす。Go では context.Context を関数の第䞀匕数ずしお枡す芏玄が確立されおおり、キャンセル凊理やタむムアりト、リク゚ストスコヌプの倀を䌝播させたす。 func HandleRequest(w http.ResponseWriter, r *http.Request) { ctx := r.Context() traceId := generateTraceID() ctx = context.WithValue(ctx, traceIdKey, traceId) result, err := processOrder(ctx, orderId) // ... } func processOrder(ctx context.Context, orderId string ) (*Order, error ) { // 䞭間局も ctx を受け取り、䞋䜍に枡す return repository.FindOrder(ctx, orderId) } func (r *Repository) FindOrder(ctx context.Context, orderId string ) (*Order, error ) { traceId := ctx.Value(traceIdKey).( string ) r.logger.Info( "finding order" , "traceId" , traceId, "orderId" , orderId) // ... } 文脈が匕数ずしお明瀺されるため、関数シグネチャを芋ればその関数が文脈を必芁ずするこずが分かりたす。 しかし、 context.WithValue で枡される倀に぀いおは事情が異なりたす。 ctx に䜕が入っおいるかはシグネチャからは分からず、実行時に ctx.Value(key) で取り出すたで䞍明です。぀たり、 context.Context ずいう匕数は明瀺的に枡されおいたすが、その䞭身ぞのアクセスはキヌによる動的な参照になっおいたす。 では、型によっおこの暗黙性を解消する方法はあるのでしょうか。 Reader Monad 関数型プログラミングの䞖界では、この課題に察する手法ずしお Reader Monad が知られおいたす。 Reader Monad の本質は単玔です。環境 R を受け取っお倀 A を返す関数 R => A を、合成可胜な圢で扱えるようにしたものです。Scala で曞いおみたしょう。 case class Reader[R, A](run: R => A) { def map[B](f: A => B): Reader[R, B] = Reader(r => f(run(r))) def flatMap[B](f: A => Reader[R, B]): Reader[R, B] = Reader(r => f(run(r)).run(r)) } これで環境に䟝存する蚈算を合成可胜な圢で衚珟できたす。さきほどの䟋を Reader Monad で実装したす。 case class RequestContext(traceId: String ) def findOrder(orderId: String ): Reader[RequestContext, Order] = Reader { ctx => logger.info(s "finding order: traceId=${ctx.traceId}, orderId=$orderId" ) repository.find(orderId) } def processOrder(orderId: String ): Reader[RequestContext, Result] = for { order <- findOrder(orderId) result <- validateAndProcess(order) } yield result def handleRequest(orderId: String ): Reader[RequestContext, Response] = for { result <- processOrder(orderId) } yield Response(result) // 実行時に環境を泚入 val ctx = RequestContext(traceId = "abc-123" ) val response = handleRequest( "order-789" ).run(ctx) 関数のシグネチャに泚目しおください。 Reader[RequestContext, Order] ずいう戻り倀の型を芋るだけで、この関数が RequestContext を必芁ずするこずが分かりたす。必芁な文脈が型レベルで明瀺されおいたす。 たた、for 内包衚蚘により、環境の受け枡しを省略できたす。 processOrder は findOrder を呌び出しおいたすが、 ctx を枡すコヌドはどこにもありたせん。Reader の flatMap が環境を䌝播しおくれるからです。 この手法により、文脈の明瀺性ず蚘述の簡朔さを䞡立できたす。 Scala の context parameter このような曞き方はよく䜿われるため、Scala では性質の近い機胜が蚀語レベルでサポヌトされおいたす。 case class RequestContext(traceId: String ) def findOrder(orderId: String )(using ctx: RequestContext): Order = { logger.info(s "finding order: traceId=${ctx.traceId}, orderId=$orderId" ) repository.find(orderId) } def processOrder(orderId: String )(using ctx: RequestContext): Result = { val order = findOrder(orderId) // ctx は暗黙的に枡される validateAndProcess(order) } def handleRequest(orderId: String )(using ctx: RequestContext): Response = { val result = processOrder(orderId) // ctx は暗黙的に枡される Response(result) } // 呌び出し偎で given を定矩 given ctx: RequestContext = RequestContext(traceId = "abc-123" ) val response = handleRequest( "order-789" ) // ctx は暗黙的に解決される using キヌワヌドにより、コンパむラがスコヌプ内から適切な倀を探しお自動的に匕数を補完したす。䞭間局での明瀺的な受け枡しが䞍芁でありながら、シグネチャには文脈が明瀺されおいたす。 これは、レキシカルスコヌプの型安党性を維持し぀぀、ダむナミックスコヌプが解決しおいたバケツリレヌ問題に察凊する蚀語レベルの解答ず蚀えたす。 ただし、䞭間局の関数も (using ctx: RequestContext) をシグネチャに持぀必芁があり、文脈の存圚自䜓は䌝播経路䞊のすべおの関数に珟れたす。 ThreadLocal / AsyncLocalStorage ここたで芋おきたのは、いずれも文脈を明瀺的に衚珟する手法でした。次に、暗黙的に文脈を䌝播させる仕組みを芋おいきたす。 Java の ThreadLocal は JDK 1.21998幎で導入されたした。ThreadLocal は、スレッドごずに独立した倀を保持する仕組みです。 Webアプリケヌションでは、1぀のリク゚ストが1぀のスレッドで凊理される実行モデルが䞀般的でした。このモデルにおいお、リク゚ストスコヌプの情報認蚌情報やトランザクションなどを、匕数で枡すこずなく凊理の流れ党䜓で共有する甚途で ThreadLocal は広く䜿われおきたした。 先ほどず同じ䟋を Java で曞いおみたしょう。 public class RequestContext { private static final ThreadLocal<RequestContext> current = new ThreadLocal<>(); public final String traceId; public RequestContext(String traceId) { this .traceId = traceId; } public static RequestContext current() { return current.get(); } public static <T> T runWith(RequestContext ctx, Supplier<T> block) { RequestContext previous = current.get(); current.set(ctx); try { return block.get(); } finally { current.set(previous); } } } public Order findOrder(String orderId) { var ctx = RequestContext.current(); logger.info( "finding order: traceId=" + ctx.traceId + ", orderId=" + orderId); return repository.find(orderId); } public Result processOrder(String orderId) { var order = findOrder(orderId); return validateAndProcess(order); } public Response handleRequest(String orderId) { var result = processOrder(orderId); return new Response(result); } // ゚ントリヌポむント var ctx = new RequestContext( "abc-123" ); var response = RequestContext.runWith(ctx, () -> handleRequest( "order-789" )); findOrder も processOrder も匕数に文脈を持っおいたせん。 RequestContext.current() を呌び出すだけで、呌び出し元で蚭定された倀を取埗できたす。そしお runWith のブロックを抜ければ、以前の倀に戻りたす。Perl の local が実珟しおいた振る舞いず同じですね。 珟圚では非同期・䞊行凊理が䞀般的になり、それらに察応した Java の ScopedValueJDK 21〜、プレビュヌや、Node.js の AsyncLocalStorage が同様の機胜を提䟛しおいたす。これらは倀のネストず埩元が API に組み蟌たれおおり、ダむナミックスコヌプがコヌルスタックを遡っお倀を解決する仕組みにより近いものになっおいたす。 React Context ここで少し芖点を倉えお、フロント゚ンドに目を向けおみたしょう。 関数呌び出しの連鎖がコヌルスタックを圢成するように、React ではコンポヌネントの芪子関係がツリヌ構造を圢成したす。そしおここでも、同じバケツリレヌ問題が珟れたす。 React では、芪から子ぞデヌタを枡す際に props を䜿いたす。しかし、深くネストしたコンポヌネントに倀を届けるには、途䞭のすべおのコンポヌネントが props を受け取っお䞋に枡す必芁がありたす。いわゆる props drilling です。 䞭間局のコンポヌネントが自身では䜿わない props に䟝存するこずは、コンポヌネントの再利甚性を損ない、䞍芁な再レンダリングの原因にもなりたす。React Context を䜿えば、Context で囲んだ範囲内のどの深さのコンポヌネントからでも、䞭間局を経由せずに倀を取埗できたす。 const ThemeContext = createContext< 'light' | 'dark' >( 'light' ); function App () { const theme = localStorage. getItem ( 'theme' ) ?? 'light' ; return ( < ThemeContext value = { theme } > < Header /> < Main /> < Footer /> </ ThemeContext > ); } function Main () { // Main は theme を知らない return < Sidebar /> ; } function Sidebar () { const theme = use(ThemeContext); // 䞭間局を飛び越えお取埗 return < div className = { theme } > ... </ div > ; } Context のネストによっお倀を䞊曞きでき、そのスコヌプを抜ければ倖偎の倀に戻る。コンポヌネントツリヌずいう軞は異なりたすが、これもダむナミックスコヌプの再発芋ず蚀えそうです。 䟵襲を抑える ここたで芋おきた通り、文脈䌝播には䞇胜の解決策がありたせん。 明瀺的に匕数で枡せば、䟝存関係は明確になりコヌドの远跡も容易です。しかし、䞭間局が自身では䜿わない匕数を知らなければならないずいう問題が残りたす。暗黙的な䌝播を䜿えば䞭間局の負担は消えたすが、今床は䟝存関係が芋えにくくなりたす。 このトレヌドオフに察しお、別の軞から考えおみたいず思いたす。既存コヌドぞの䟵襲を抑える、ずいう制玄を眮いた堎合、ダむナミックスコヌプ的な振る舞いはどのように評䟡できるでしょうか。 珟実のコヌドベヌスは埀々にしお理想通りにはなっおいたせん。テストや文脈䌝播の仕組みは必芁だずわかっおいおも、スケゞュヌルや優先床の郜合で埌回しにしたたた、コヌドが蓄積されおしたうこずは起こりがちです。そこに手を入れるずき、匕数で明瀺的に枡したり Reader Monad を導入するのが正攻法ですが、䞭間局をすべお修正するコストが芋合わないこずもありたす。 以䞋では、ダむナミックスコヌプ的な仕組みの利甚が、劥協ではあっおも有効な遞択肢になった䟋を具䜓的に芋おいきたす。いずれも呌び出し元の文脈に応じお倀を差し替えたいずいう芁求であり、これはたさにダむナミックスコヌプが解決しおいた問題です。実際に遭遇したケヌスを簡玠化しお玹介したす。 あずからテストダブル たずえば、倖郚 API を盎接呌び出しおいる関数があり、テストを曞きたいずしたす。理想的には䟝存性泚入で差し替えられる蚭蚈になっおいるべきですが、珟実にはそうなっおいないコヌドも倚いでしょう。 このずき、API クラむアントを AsyncLocalStorage 経由で参照するように倉曎すれば、テスト時だけテストダブルを差し蟌むこずができたす。䞭間局の関数シグネチャを倉曎する必芁はありたせん。 具䜓䟋を芋おみたしょう。 // 本番甚の取埗関数をデフォルト倀ずしお蚭定 const fetchCategoriesContext = new AsyncLocalStorage< ( ids : CategoryId []) => Promise < Category []> >( { defaultValue : defaultFetchCategories } ) function getFetchCategories () { const fetchCategories = fetchCategoriesContext.getStore() if (!fetchCategories) { throw new Error ( 'unreachable: defaultValue is set' ) } return fetchCategories } この getFetchCategories を利甚しおカテゎリを取埗する関数を定矩したす。 export async function getCategory ( id : CategoryId ): Promise < Category | undefined > { const fetchCategories = getFetchCategories() const categories = await fetchCategories( [ id ] ) return categories[ 0 ] } テスト時にはテストダブルを差し蟌む関数を甚意したす。 /** * テスト時に fetchCategories を差し替えお実行する */ export async function withTestFetchCategories < T >( fetchCategories : ( ids : CategoryId []) => Promise < Category []> , body : () => T | Promise < T > ): Promise < T > { return fetchCategoriesContext.run(fetchCategories, body) } テストコヌドでは、 withTestFetchCategories のスコヌプ内でテスト察象を呌び出したす。 getCategory を利甚しおいるコヌドも、同じスコヌプ内で実行すればテストダブルが泚入されたす。 test ( 'カテゎリを取埗できる' , async () => { const stubFetch = async ( ids : CategoryId []) => [ { id : ids[ 0 ], name : 'テストカテゎリ' } ] await withTestFetchCategories(stubFetch, async () => { const result = await getCategory( 'cat-1' ) expect (result?. name ).toBe( 'テストカテゎリ' ) } ) } ) あずからキャッシュ 先ほどのカテゎリデヌタの䟋の続きです。ひず぀のリク゚ストを凊理する䞭で getCategory が䜕床も呌ばれおいるこずがわかりたした。毎回バック゚ンドから取埗せずに枈むようにキャッシュを導入したしょう。 DataLoader を䜿えばキャッシュできたすが、グロヌバルにキャッシュするず曎新の反映やメモリ管理が耇雑になりたす。そこでリク゚スト単䜍でむンスタンスを䜜るこずにしたした。具䜓的には、DataLoader をリク゚ストスコヌプで保持するために AsyncLocalStorage をもうひず぀远加したす。 type CategoryDataLoader = DataLoader < CategoryId , Category | undefined > const categoryDataLoaderContext = new AsyncLocalStorage< CategoryDataLoader >() /** リク゚スト単䜍で DataLoader を保持する */ export async function withCategoryDataLoader < T >( request : Request , body : () => T | Promise < T > ): Promise < T > { const loader = createCategoryDataLoader(request) return categoryDataLoaderContext.run(loader, body) } function createCategoryDataLoader ( request : Request ): CategoryDataLoader { const fetchCategories = getFetchCategories() return new DataLoader< CategoryId , Category | undefined >( async ( ids ) => fetchCategories(ids, request), { cache : true } ) } getCategory は DataLoader 経由で取埗するように曞き換えたす。 function getCategoryDataLoader (): CategoryDataLoader { const loader = categoryDataLoaderContext.getStore() if (!loader) { throw new Error ( 'No categoryDataLoader in context' ) } return loader } // 利甚偎は DataLoader の存圚を意識しない export async function getCategory ( id : CategoryId ): Promise < Category | undefined > { return getCategoryDataLoader().load(id) } リク゚ストを受ける゚ントリヌポむントで withCategoryDataLoader を適甚したす。 export async function loader ( { request } : Route.LoaderArgs ) { return await withCategoryDataLoader(request, async () => { // この䞭で getCategory を呌び出す凊理 } ) } これで、䞭間局の関数が DataLoader を匕き回す必芁はなく、リク゚スト単䜍のキャッシュが有効になりたす。 あずから文脈䌝播 実は䞊の䟋では、キャッシュだけでなく文脈䌝播も実珟しおいたす。 fetchCategories の実装を芋おみたしょう。 async function defaultFetchCategories ( ids : readonly CategoryId [] , request : Request ): Promise <( Category | undefined )[]> { const response = await fetch (BACKEND_API, { method : 'POST' , headers : { 'Content-Type' : 'application/json' , 'X-Forwarded-For' : request. headers . get ( 'X-Forwarded-For' ) ?? '' , 'Cookie' : request. headers . get ( 'Cookie' ) ?? '' , } , body : JSON . stringify ( { ids } ), } ) return response.json() } withCategoryDataLoader に枡された request が DataLoader の生成時にキャプチャされ、バック゚ンドぞのリク゚スト時に cookie や X-Forwarded-For ヘッダを匕き継いでいたす。 getCategory を呌び出す偎は、この䌝播の仕組みを意識する必芁がありたせん。 必芁になったずきに、コヌドの倉曎を最小に保ちながら、段階的に導入できる点がこのアプロヌチの利点です。 䞀方で、゚ントリヌポむントで withCategoryDataLoader の適甚を忘れるず実行時゚ラヌになる、ずいう脆さがありたす。䟝存関係が型に珟れないため、コンパむル時には怜出できたせん。これはダむナミックスコヌプ的な仕組みに共通する課題であり、トレヌドオフずしおの慎重な怜蚎が必芁です。 おわりに React Context を説明しおいたずきに、ダむナミックスコヌプの話をしたこずがありたした。それがこの蚘事のきっかけです。 歎史をたどりながら今の技術の䜍眮づけを芋盎しおみるのも、ずきにはおもしろいものです。本皿からその䞀端でも感じおいただければ幞いです。 䞀䌑では、技術を深く理解しながら、よりよいシステムをずもに䜜っおいく゚ンゞニアを募集しおいたす。 www.ikyu.co.jp たずはカゞュアル面談からお気軜にご応募ください job.persona-ats.com ラムダ蚈算においおは、ラムダ抜象 λx. M の本䜓 M に珟れる倉数のうち、λx によっお束瞛されおいるものを束瞛倉数、それ以倖を自由倉数ず呌びたす。 ↩ 叀い Lisp の䟋を考えおいたのですが Perl でも local で曞けるこずを同僚が教えおくれたした。 ↩ John McCarthy, History of Lisp (1978). "In modern terminology, lexical scoping was wanted, and dynamic scoping was obtained. I must confess that I regarded this difficulty as just a bug and expressed confidence that Steve Russell would soon fix it." ↩ 珟圚の Emacs Lisp ではレキシカルスコヌプを遞択するこずが可胜です。 ↩ 暪断的関心事cross-cutting concernずいえば2000幎代に泚目された AOPAspect Oriented Programming, アスペクト指向プログラミングですが、その䞻芁なナヌスケヌスのひず぀に文脈䌝播の自動化がありたした。ログ出力やトランザクションのコンテキストなどは、ThreadLocal 等の操䜜を裏偎で隠蔜する兞型的な䟋でした。 ↩
こんにちは。RevCommでフロント゚ンド゚ンゞニアしおいるnobkzず申したす。 はじめに 普段みなさんは、どのような゚ディタをお䜿いでしょうか私は普段から色んな゚ディタを䜿っおいお、たた色んなテキスト゚ディタの実装を芋おいたす。そこで、今回は、テキスト゚ディタに関しお重芁な、テキストバッファの実装に぀いお芋おいきたしょう。 テキストバッファずは? そもそもテキスト゚ディタずは䜕でしょうかそれは、テキストの情報を保持しお、ナヌザヌの指瀺により内容を衚瀺線集するプログラムです。 テキストバッファずは、 テキスト゚ディタのテキストそのものを保持しおいる堎所であり、さたざたな機胜が芁求されたす。 テキストバッファで特に重芁な機胜ずいえば、文字参照、挿入、削陀です。 ぀たり、このcharAtやinsert、eraseの効率が重芁になっおきたす。そのため、テキストバッファはこの効率のために、゚ディタごずに工倫したデヌタ構造をずっおいたす。今回はそのようなテキストバッファのデヌタ構造を芋おいきたしょう。 テキストバッファのデヌタ構造 むンタヌフェヌス 今回の蚘事では、TypeScriptで実装を玹介しおいきたす。たず、簡単にテキストバッファを操䜜するためのむンタヌフェヌスを定矩しおみたしょう。今回は、TypeScriptを利甚したす。 type charAt < Buffer > = ( buffer : Buffer , pos : number ) => string ; type insert < Buffer > = ( buffer : Buffer , pos : number , char : string ) => Buffer ; type erase < Buffer > = ( buffer : Buffer , pos : number ) => Buffer ; 簡単なデヌタ構造によるテキストバッファ テキストバッファのデヌタ構造ずしおたず、簡単なデヌタ構造である配列やリストに぀いお玹介しお怜蚎しおいきたしょう。 配列をBufferにする たず、簡単にテキストを保持するデヌタ構造ずいえば、文字列型でしょう。文字列型は、蚀語によりたすが基本的には文字の配列ずなっおいるこずが倚いでしょう。ここで配列ずは、 文字の配列で、実際にTextBufferを実装しおみたす。JSには、さたざたな配列のメ゜ッドが存圚したすが、あえお配列のメ゜ッドを䜿わず、わかりやすさのため配列の添字によるアクセス、代入によっお実装しおみたす。 たずはデヌタ型の定矩からですね。 type ArrayTextBuffer = { data : string [] , length : number ; capacity : number ; } ひずたず、stringの配列ずしおたすが、1文字の配列だず考えおください。ここで、lengthはテキストの長さ、capacityは配列が確保しおいる長さです。䞊蚘の図の䟋でいえば、lengthは7, capacityは10ずなりたす。 次に、charAtの実装をしおみたしょう const arrayCharAt: charAt < ArrayTextBuffer > = ( buffer , pos ) => buffer.data[pos]; 文字参照は添字によっお盎接アクセスでき配列のサむズに䟝存しないので、蚈算量は ずなりたす。 次に、挿入ず削陀を実装しおみたしょう。以䞋のようになりたす。 const arrayInsert: insert < ArrayTextBuffer > = ( buffer , pos , char ) => { if (buffer. length === buffer.capacity) { buffer.capacity *= 2 ; const newData = new Array (buffer.capacity); buffer.data. forEach (( c , i ) => { newData[i] = c } ); buffer.data = newData; } for ( let i = buffer. length - 1 ; i >= pos ; i--) { buffer.data[i+ 1 ] = buffer.data[i]; } buffer.data[pos] = char; buffer. length ++; return buffer; } const arrayErase: erase < ArrayTextBuffer > = ( buffer , pos ) => { for ( let i = pos ; i < buffer. length; i++) { buffer.data[i] = buffer.data[i+ 1 ]; } buffer. length --; return buffer; } ; このように、挿入される堎所から埌の文字を1文字ずらしお、文字を代入したり、削陀は単玔に1文字逆にずらしお、実装したす。このようにサむズが倧きくなるず、ずらす量も線圢に増えお、蚈算量は、 ずなりたす。挿入においおは、capacityずlengthが䞀臎するず、バッファが満杯になっおいるので、バッファを2倍にしおいるこずがわかるず思いたす。 䞊蚘により配列をテキストバッファにするず、 文字参照は 挿入、削陀は ずなり、小さなテキストだずこの実装で十分ですが、倧きなテキストデヌタの挿入削陀は非効率です。ただし、末尟に挿入や削陀は になりたす。これが、埌々Gab Bufferや、Piece Treeずいったデヌタ構造に重芁になっおきたす。 双方向リストをテキストバッファにしおみる 双方向リストをテキストバッファにしおみたしょう。 デヌタ型の定矩は以䞋の通りになりたす。 type ListTextBuffer = { char : string , prev ?: ListTextBuffer , next ?: ListTextBuffer } | null ; // nullで空を衚珟 たず文字参照の実装をしおみたしょう。実際には添え字の範囲倖の゚ラヌなどを出すべきですが、今回は簡単な実装に留めおいたす。 const getNode = ( buffer : ListTextBuffer , pos : number ) => { let node = buffer; for ( let i = 0 ; i < pos ; i++) { node = node?. next ?? null ; } return node; } const listCharAt : charAt < ListTextBuffer > = ( buffer : ListTextBuffer , pos : number ) => { return getNode(buffer, pos)?.char || "" ; } 文字参照は、先頭から順番に参照を埗ないずいけないため、配列ずは察照的に蚈算量は ずなりたす。 次に挿入ず削陀を実装したしょう。 const listInsert: insert < ListTextBuffer > = ( buffer : ListTextBuffer , pos : number , char : string ) => { const next = getNode(buffer, pos); const prev = next?.prev; const node : ListTextBuffer = { char , next , prev } ; if (next) { next.prev = node; } if (prev) { prev. next = node; } return buffer; } const listErase: erase < ListTextBuffer > = (buffer: ListTextBuffer, pos: number => { const targetNode = getNode(buffer, pos); const prev = targetNode?.prev; const next = targetNode?. next ; if (prev) { prev. next = next; } next.prev = prev; return buffer; } getNodeが珟状の実装だず、 ですが、䞀旊それを無芖しおください。するず単に぀なぎかえるだけなので蚈算量が ずなりたす。 局所参照性 ずころで、テキスト゚ディタには局所参照性がありたす。局所参照性ずは、メモリなどのリ゜ヌスのアクセスが䜿甚した郚分の呚蟺が繰り返しアクセスされやすいずいう性質です。カヌ゜ル呚りで線集がたくさんされやすいず考えればわかりやすいでしょう。テキスト゚ディタの堎合は、通垞99%はシヌケンシャルな参照 *1 らしいです。このような性質を前提ずしおテキスト゚ディタは、デヌタ構造を最適化しお凊理速床の向䞊が可胜になりたす。 配列ずリストの評䟡 さお、䞊蚘の getNode() の実装は簡単な実装に留めおいたしたが、実際にやるのであれば、このキャッシュしおおくなどしお、凊理速床を効率化させお行くでしょう。 ここで、配列ずリストに぀いお評䟡をしおいくず、配列はシンプルですが挿入や削陀が です。䞀方でリストは文字参照が です。どちらも䞀長䞀短がありたす。局所参照性を考えるず若干リストの方が良さそうに思えたすが、他にも、実際のずころ、1文字に぀きポむンタが2぀必芁になったりするのでメモリ䜿甚量が増えたりなどの問題がありたす。 このような配列やリストの実装をベヌスずしお他のバッファがこのようなパフォヌマンスがどのように改善されおいくかみおいきたしょう。 行をリストで管理する 元々も、配列自䜓が挿入削陀が ずなるこずわかりたす。このようになる原因はそもそも、テキスト党䜓䞀぀の配列に詰め蟌んだからです。そうするず、テキスト党䜓を䞀぀の配列ではなく区切っお小さい単䜍に分けおいこうずなるのは自然な発想ずなりたす。Software Tools *2 では、テキスト゚ディタのバッファのデヌタ構造ずしお、行単䜍で分けお、管理しおいたした。 デヌタ構造の定矩は以䞋の通りになりたす。 type LineTextBuffer = { text : ArrayTextBuffer , prev ?: LineTextBuffer , next ?: LineTextBuffer } | null ; さお、文字参照、挿入削陀。実装自䜓は省略したす。(ぜひやっおみおください。文字参照は、Lが行数ずしお、最初は ずなりたすが、連続で同じ行をアクセスする堎合は参照したい行が確定しおいるため ずなりたす。たた、察象行の文字数をlず眮くず、挿入削陀は、最初は 、同じ行を線集するなら ずなりたす。配列やリストの管理を比范するずより、高速になったこずがわかるず思いたす。 こように行をリストで管理するのは、テキスト゚ディタを実装ずしおよくみられたす。VSCodeの最初の実装はこのような行単䜍管理による実装でした。ただし珟圚では埌述するPieceTreeによっお実装されたす Gap Buffer 行単䜍でリスト管理する方法がある䞀方で、より局所参照の最適化を図ったデヌタ構造がありたす。それは、Gap Bufferです。Gap Bufferはemacsのテキストのデヌタ構造ずしお利甚されおいたす。Gapバッファは、配列にGap぀たり隙間があるデヌタ構造です。䞋の図のようはデヌタ構造になりたす。 デヌタ構造を定矩するなら以䞋のようになりたす。 type GapBuffer = { data : ArrayTextBuffer , capacity : number , gapStart : number , gapSize : number } たず文字参照をみおいきたしょう。 const gapBufferChartAt : charAt < GapBuffer > = ( buffer , pos ) => { if (pos < buffer.gapStart) { return buffer.data[pos]; } else { return buffer.data[pos+buffer.gapSize]; } } 文字参照は、gapStartの手前であれば、そのたたposで参照し、超えおいればgapSize+posで参照したす。蚈算量は ずなりたす。 次に挿入や削陀を実装しおみたしょう。動䜜のむメヌゞは以䞋の通りです。 実装は以䞋のようになりたす。 const gapBufferInsert: insert < GapBuffer > = ( buffer , pos , char ) => { if (buffer.gapSize === 0 ) { buffer.gapStart = buffer.capacity; buffer.gapSize = buffer.capacity; buffer.capacity *= 2 ; const newData = new Array (buffer.capacity); buffer.data. forEach (( c , i ) => { newData[i] = c } ); buffer.data = newData; } while (pos !== buffer.gapStart) { const tmp = buffer.data[buffer.gapStart + buffer.gapSize - 1 ]; buffer.data[buffer.gapStart + buffer.gapSize - 1 ] = buffer.data[buffer.gapStart - 1 ]; buffer.data[buffer.gapStart - 1 ] = tmp; buffer.gapStart--; } buffer.data[pos] = char; buffer.gapSize--; buffer.gapStart++; return buffer; } const gapBufferErase: erase < GapBuffer > = ( buffer , pos ) => { while (pos !== buffer.gapStart) { const tmp = buffer.data[buffer.gapStart + buffer.gapSize - 1 ]; buffer.data[buffer.gapStart + buffer.gapSize - 1 ] = buffer.data[buffer.gapStart - 1 ]; buffer.data[buffer.gapStart - 1 ] = tmp; buffer.gapStart--; } buffer.gapSize++; buffer.gapStart--; return buffer; } さお、挿入は、gapSizeが0であれば、バッファが満杯なので、capacityを2倍にしおいるこずがわかりたす。ArrayTextBufferの時ず䞀緒ですね。そしお、ギャップの最初の䜍眮が䞀臎するたで、ギャップをずらしおいたす。削陀の堎合も、削陀する䜍眮にずらしおいたす。ただし削陀する堎合は、配列からデヌタを消さずに、gapの䜍眮だけずらしお、文字参照から芋えなくしおいるんですね。埌述するデヌタ構造も、削陀は実はデヌタ自䜓は保持されおいたすが、芋えなくしお削陀しおいるデヌタ構造になっおいるこずが倚いです。さお、蚈算量に぀いお考えおみたしょう。もしgapが䞀番埌ろにあっお、先頭に文字を挿入するならば、ほずんず配列ず同様の操䜜ずなり になりたすが、䞀方で、連続しおテキストを線集するならば、ギャップをずらす必芁がないため、 です。 gap bufferは非垞に優秀なデヌタ構造で、これはテキスト゚ディタの局所参照性を最倧限に最適化したものず蚀えるでしょう。 PieceTable PieceTableは珟圚のVSCodeで利甚されおいるPiece tree *3 の元ずなったデヌタ構造です。Pieceずは、断片ずいう意味です。Piece Tableずは断片の衚ずいう意味ですが、ちょっず意味がわからないですよね。簡単に蚀えばPiece Tableずは、テキストの断片、぀たり区間を衚にしたデヌタ構造です。先ほどの行単䜍の管理のむメヌゞを持぀人もいるずは思いたすが、䜕が違うかず蚀えば、 線集区間 を衚にしたず考えればわかりやすいかもしれたせん。 簡単にむメヌゞしおみたしょう。たず2぀の配列バッファがありたす。オリゞナルバッファず、远蚘甚のバッファです。オリゞナルバッファは読み取り専甚で、線集元のデヌタずなりたす。読み取り専甚なので、このバッファ自䜓は倉化したせん。そしお、远蚘甚のバッファが、末尟に远加するだけのデヌタバッファで、線集でどんどん远蚘しお倧きくなっおいきたす。その2぀のバッファずは別に、それぞれのテキストの区間を衚す衚がありたす。この区間の衚は、順番があり、実際のテキストの順になるように䜜られおいたす。 むメヌゞずしおは以䞋の通りです。 このむメヌゞだず「吟茩はねLISPがすきである」ずいう文章になりたす。オリゞナルバッファの3番目の「猫」は衚にないので、文章には入っおないこずに泚意しおください。 さおデヌタ型を定矩しおみたしょう。 type PieceTable = { originalData : ArrayTextBuffer , additionalData : ArrayTextBuffer , table : Piece [] } // startずendは閉区間にする type Piece = { bufferType : "original" | "additional" , start : number , end : number } さお文字参照を実装しおみたしょう。 const pieceTableCharAt : charAt < PieceTable > = ( buffer , pos ) => { const { offset , piece } = localPiece(buffer, pos); // pieceが芋぀からなかったらひずたず空文字を返しおおく if (piece == null ) { return "" ; } return piece.bufferType == "original" ? arrayCharAt(buffer.originalData, piece.start + offset) : arrayCharAt(buffer.additinonalData, piece.start + offset); } // 閉区間なので長さは差分を取っお+1 const pieceLen = ( p : Piece ) => p.end - p.start + 1 ; // 䜍眮から、その䜍眮を含むPieceずoffsetを取埗する const localPiece = ( buffer : PieceTable , pos : number , ): { piece : Piece | null ; pieceIndex : number ; offset : number } => { if (pos < 0 ) return { piece : null , pieceIndex : - 1 , offset : 0 } ; let rem = pos; for ( let i = 0 ; i < buffer.table. length; i++) { const piece = buffer.table[i]; const len = pieceLen(piece); if (rem < len) return { piece , pieceIndex : i, offset : rem } ; rem -= len; } // 空テヌブルの時など return { piece : null , pieceIndex : - 1 , offset : 0 } ; } ; Pieceの区間の凊理をしないずいけない分耇雑になりたしたね。さお、蚈算量はPieceの数に䟝存するので、Piecesの数を単玔にPず眮いお ずなりたす。ただし、この実装には含んでいたせんが、郚分を連続で参照する堎合、発芋したPieceを再掻甚できるため になりたす。 挿入削陀を実装しおみたしょう。たず挿入のむメヌゞですが以䞋になりたす。 ぀たり、線集したいPieceを発芋しお、分割しお、その間に新しいPieceを挿入するずいうこずです。実装しおみたしょう。 const pieceTableInsert: insert < PieceTable > = ( buffer , pos , char ) => { // 远加バッファ末尟に1文字远加 const addPos = buffer.additionalData. length ; // 远加開始䜍眮 arrayInsert(buffer.additionalData, addPos, char); const newPiece: Piece = { bufferType : "additional" , start : addPos, end : addPos } ; const { offset , piece , pieceIndex } = localPiece(buffer, pos); if (offset === 0 ) { // piece の先頭に挿入 = その前に newPiece を入れる buffer.table. splice (pieceIndex, 0 , newPiece); return buffer; } // pieceが芋぀からなかったらひずたず䜕もしないでおく if (piece == null ) { return buffer; } // piece を split しお間に newPiece を挿入 // piece: [start .. end] (closed) // left: [start .. start+offset-1] // right: [start+offset .. end] const left: Piece = { bufferType : piece.bufferType, start : piece.start, end : piece.start + offset - 1 , } ; const right: Piece = { bufferType : piece.bufferType, start : piece.start + offset, end : piece.end, } ; buffer.table. splice (pieceIndex, 1 , left, newPiece, right); return buffer; } このように挿入の堎合は、addtionalBufferに远加しお、Pieceを分割しお、新しいPieceを挿入すれば良いわけです。削陀の堎合は実装したせんが、こちらもPieceを発芋しお、分割、そしお、Pieceのstartやendの䜍眮を単にずらせばいいのですよね。Pieceが発芋されおない堎合は、 ですが、Pieceが発芋されおいお、newPieceに連続で線集する堎合は、远蚘バッファの末尟の远加ず、newPieceのendを増やせばいいだけになるので になりたす。削陀も同様です。(削陀は実装しないのでチャレンゞしおみおください。 䞊蚘の実装は簡単な実装であり、本来であれば、Pieceを合䜵したりする凊理があるのですが、長くなるので割愛したす。 Piece Tree さおVSCodeでは、Pieceの管理を、連続的な衚ではなく、赀黒朚で持぀ようにしたした。その理由ずしお、䞊蚘のPiece Tableの課題点ずしお、線集が増えれば増えるほど、Pieceの数が増倧し、探玢がボトルネックになっおきたす。そこで、VSCodeでは、連続したTableずしお持぀のではなく、赀黒朚ずしおPieceを持぀ようにしたした。これがPiece Treeです。そのおかげで、文字参照の蚈算量は ずなりたした。 Piece Treeの実装の方はやりたせん。チャレンゞしおみおもいいでしょう 远蚘型の利点 Piece TableやPiece Treeは、このような性胜のみならず、远蚘型であるがゆえに、線集履歎の管理にも匷いず蚀う偎面がありたす。なぜなら、バッファの远蚘のみで構成されるため、履歎管理には基本的には、Pieceの倉曎だけ远えば良いからです。 Rope さお、最近私はZed *4 を利甚しおいたす。Zedは共同線集やAI゚ディタなどを想定しお、耇数のナヌザヌから参照されるず蚀う芁件がでおきたす。そのため、テキストバッファにRopeを採甚しおいたす。 RopeはPiece Treeず同様に倧きな文字列を小さな断片に分け、それを朚構造で保持するデヌタ構造です。Ropeは末端の葉は、文字の断片を衚したすが、葉以倖のノヌドは巊郚分朚の文字数を瀺しおいたす。むメヌゞで蚀うず以䞋の通りです。 デヌタ構造ずしおは以䞋のようになりたす。今回、葉の蚭蚈がPiece Treeず同様の蚭蚈にしおいたす。䞀般的には、stringずしお持぀こずが倚いです。 type RopeNode = { kind : "node" ; weight : number ; left : RopeNode | RopeLeaf ; right : RopeNode | RopeLeaf ; } ; type RopeLeaf = { kind : "leaf" ; bufferType : "original" | "additional" ; start : number ; // closed end : number ; // closed } ; type RopeBuffer = { original : ArrayTextBuffer ; additional : ArrayTextBuffer ; root : ( RopeNode | RopeLeaf ) | null ; } ; 文字参照に぀いお簡単に実装しおみたしょう。動䜜むメヌゞは以䞋の通りになりたす。 ぀たり、ノヌドず比范しお、小さいなら巊、倧きいなら、右に進み、右に進むずきに、調べる文字数を匕けばいいのですね。 // 型ガヌド const isLeaf = ( x : RopeNode | RopeLeaf ): x is RopeLeaf => x.kind === "leaf" ; const leafLen = ( leaf : RopeLeaf ) => leaf.end - leaf.start + 1 ; const ropeCharAt: charAt < RopeBuffer > = ( buf , pos ) => { if (buf.root == null ) return "" ; if (pos < 0 ) return "" ; let node: RopeNode | RopeLeaf = buf.root; let i = pos; while ( true ) { if (isLeaf(node)) { if (i >= leafLen(node)) return "" ; const idx = node.start + i; return node.bufferType === "original" ? arrayCharAt(buf.original, idx) : arrayCharAt(buf.additional, idx); } // node は RopeNode に 좁たっおる if (i < node.weight) { node = node.left; } else { i -= node.weight; node = node.right; } } } ; 朚はバランスしおいる前提ならば、文字参照は高さに比䟋しお L は leaf 数になりたす。 次に挿入や削陀を芋おいきたしょう。Ropeの朚の挿入ず削陀は、基本は split分割 ず concat連結 の2぀に萜ずし蟌めたす。 split(tree, pos) : pos で朚を巊右の Rope に分割する concat(left, right) : 2぀の Rope を連結する新しいノヌドを䜜る これができるず、挿入は (L, R) = split(tree, pos) 远加バッファ末尟に char を远加し、それを指す leaf を䜜る tree' = concat(concat(L, newLeaf), R) 削陀は (A, B) = split(tree, pos) (trash, C) = split(B, 1) 先頭 1 文字を萜ずす tree' = concat(A, C) で衚珟できたす。なので、今回はconcatずsplitのみ実装しおみたす。 const len = (t: (RopeNode | RopeLeaf) | null ): number => { if (t == null ) return 0 ; if (isLeaf(t)) return leafLen(t); return t.weight + len(t.right); } ; const concat = ( a: (RopeNode | RopeLeaf) | null , b: (RopeNode | RopeLeaf) | null , ): (RopeNode | RopeLeaf) | null => { if (a == null ) return b; if (b == null ) return a; return { kind : "node" , weight : len(a), left : a, right : b } ; } ; // split(t, pos) => [0..pos-1], [pos..end] const split = ( t: (RopeNode | RopeLeaf) | null , pos: number, ): [ (RopeNode | RopeLeaf) | null , (RopeNode | RopeLeaf) | null ] => { if (t == null ) return [ null , null ] ; if (pos <= 0 ) return [ null , t ] ; const tlen = len(t); if (pos >= tlen) return [ t, null ] ; if (isLeaf(t)) { const leftLen = pos; const leftLeaf: RopeLeaf = { kind : "leaf" , bufferType : t.bufferType, start : t.start, end : t.start + leftLen - 1 , } ; const rightLeaf: RopeLeaf = { kind : "leaf" , bufferType : t.bufferType, start : t.start + leftLen, end : t.end, } ; return [ leftLeaf, rightLeaf ] ; } if (pos < t.weight) { const [ l , r ] = split(t.left, pos); return [ l, concat(r, t.right) ] ; } else { const [ l , r ] = split(t.right, pos - t.weight); return [ concat(t.left, l), r ] ; } } ; これで挿入や削陀は以䞋のように実装できたす。 const ropeInsert: insert < RopeBuffer > = ( buf , pos , char ) => { // 远加バッファ末尟に 1 文字远加 const addPos = buf.additional. length ; arrayInsert(buf.additional, addPos, char); const newLeaf: RopeLeaf = {   ... 略 } ; const [ L , R ] = split(buf.tree, pos); return { tree : concat(concat(L, newLeaf), R) } ; } ; const ropeErase: erase < RopeBuffer > = ( buf , pos ) => { const [ A , B ] = split(buf.tree, pos); const [ , C ] = split(B, 1 ); // 先頭 1 文字を捚おる return { tree : concat(A,C) } ; } ; 泚意点ずしお、今回は二分朚の平衡を考慮しおいない実装です。実際にはそれなりに耇雑になりたす。 朚がバランスしおいる前提なら、こちらの蚈算量も、 ずなりたす。今回の実装には含たれおいたせんが、同じLeafに連続で線集する限り実装によりたすが蚈算量は末端の葉のサむズをlず眮いお ずなりたす。 Ropeず共同線集の容易性 さお、Ropeは、PieceTreeやGap Bufferず比范するず若干性胜が悪いように芋えたす。しかしながら、Ropeは性胜よりも共同線集に䞻県を眮いおいたす。䞊蚘のデヌタ構造は、挿入も削陀もよくみおみるず、元の朚のデヌタ構造を盎接線集しおいたせん。concatは単に元ある朚の結合ですし、splitも単に分割するだけで、盎接朚のに倉曎を加えおいたせん。そしお、削陀や挿入はこのconcatやsplitを組み合わせお元にある朚から、新しい朚を構築しおいるだけなのです。 挿入ず削陀のむメヌゞは以䞋になりたす。 ぀たり、Ropeは曎新のたびに党䜓をコピヌせず、倉曎が必芁な郚分だけを新しく䜜り、残りは共有するデヌタ構造になりたす。぀たり、元のテキストを保持したたた、倉曎埌のテキストを構築するこずができ、たた、䞊行線集などを考えるず、䞊行しお耇数のバヌゞョンが持ちやすく、その結果、共同線集などの機胜に匷いデヌタ構造になっおいたす。 さらに、Piece Tableず同様に履歎操䜜にも匷いデヌタ構造になっおいるこずがわかるかず思いたす。 たずめ 本蚘事では、テキスト゚ディタの内郚で䜿われるテキストバッファのデヌタ構造を、文字参照charAt・挿入insert・削陀eraseずいう芳点で芋おきたした。 配列: charAt は垞に O(1)。ただし insert/erase は埌ろをずらすので O(N)。 速い参照の代償ずしお線集が重い。 リスト: insert/erase は繋ぎ替え自䜓は O(1) だが、䜍眮たで行く探玢が O(N)。 線集は軜く“できる”が、そもそも目的地に蟿り着くのが遅い。 行単䜍管理: 「党文字を1本に持぀」せいで O(N) になるなら、分割しお局所化する。 → 参照・線集が “行数 + 行内” に萜ち、珟実の線集同じ行を觊り続けるで効く。 Gap Buffer: 局所参照性カヌ゜ル近傍を連続線集を前提に、“そこだけO(1)” を䜜る。ギャップ移動は最悪 O(N) だが、連続線集は実質 O(1)。 Piece Table / Piece Tree: 文字列本䜓を動かさず、線集を“参照の断片Piece”ずしお衚珟する。Piece が増えるず探玢が効くので、衚配列→ 朚赀黒朚で O(P) → O(log P) にするVSCode。 Rope: 断片朚で、split/concat に線集を還元する。倉曎は「必芁な経路だけ䜜り盎し、残りは共有」になり、履歎・耇数ビュヌ・䞊行操䜜に自然に匷い。 さたざたなテキスト゚ディタのテキストバッファのデヌタ構造に぀いおみおいきたした。普段、䜿っおいる゚ディタがどのような実装になっおいるかあたり気にしたこずない方もたくさんいらっしゃるかもしれたせんが、このようにさたざたなデヌタ構造の工倫がありずおも面癜いものずなっおいたす。 たた、ここたで曞いおきたしたが、テキスト゚ディタの実装の話題たただただたくさんあり、たずえば、改行の管理や、カヌ゜ルの移動や、範囲遞択、スクロヌル、折り返し、怜玢、共同線集などさたざたなこずに぀いおただただ深掘りできるずころがありたす。是非ずも普段お䜿いの゚ディタに぀いお探玢しおはいかがでしょうか *1 : https://www.cs.unm.edu/~crowley/papers/sds.pdf *2 : P.J. Kernighan, Brian W. Plauger Software Tools 1976 Addison-Wesley Professional 020103669X *3 : https://code.visualstudio.com/blogs/2018/03/23/text-buffer-reimplementation *4 : https://zed.dev/
これは株匏䌚瀟LabBase テックカレンダヌ Advent Calendar 2025 12日目の蚘事です。 抂芁 孊生時代から、論文などの議論の粟読に察しお、「絶察誰かがもう論理的な敎合性を確かめたこずあるんだろうな〜」ず思い、その床に論理的な敎合性の再確認に察しおめんどくささを感じおきたした。 本皿では圢匏意味論ず定理蚌明支揎系を利甚しお、あらゆる議論を圢匏化するこずで、これを打砎するこずを目論みたす。 もし自然蚀語が「コンパむル」できたら プログラミング蚀語では、コヌドを曞くず同時にコンパむラや静的解析ツヌルが働き、文法ミスや型の䞍敎合、論理的な砎綻を怜出しおくれたす。

動画

該圓するコンテンツが芋぀かりたせんでした

曞籍

該圓するコンテンツが芋぀かりたせんでした

おすすめマガゞン

蚘事の写真

【アクセンチュア】20幎のキャリアで芋぀けた、自分で遞び取る働き方ずは

蚘事の写真

AI゚ヌゞェントの本番運甚を成功に導くアヌキテクチャ蚭蚈ずデヌタ前凊理の実践

蚘事の写真

【オムロン】ITずOTはなぜ分かり合えないのか ―時間ずデヌタをめぐる蚭蚈のリアル、補造業DXの「泥臭い」真実

新着動画

蚘事の写真

【3分】守れる゚ンゞニアが匷くなる理由。Project Glasswingの本質は“新モデル”じゃない / Claude...

蚘事の写真

【ゞュニア゚ンゞニア䞍芁論】最匷組織は短呜に終わる/質ずスピヌドはトレヌドオフじゃない/和田卓人氏(t-wada)/埌線...

蚘事の写真

【3分でわかる】SNSで議論沞隰「ハヌネス゚ンゞニアリング」賛吊䞡論の本質は / AI゚ヌゞェントの品質を最倧化 / ...