OCaml 5.5詳解——module依存のfunction、polymorphicな引数、GCと配布の変更

OCaml 5.5のmodule-dependent functionとpolymorphic parameterを実行し、文字列API、external type、GC、5.5.2までの修正を詳しく解説します。

Share
OCaml 5.5の表題と、moduleが提供する型とoperationをfunctionへ接続するカバー
OCaml · 5.5

OCaml 5.5.0は2026年6月19日、5.5.1は9月5日、5.5.2は10月23日に公開されました。 5.5.0で増えた言語機能を中心に、5.5.2までのcompilerとruntimeの修正も扱います。 コード例の対象はmacOS/arm64のOCaml 5.5.0です(5.5.0、5.5.1、5.5.2の公開記録)。

5.5では、moduleの型を後続の引数や返り値へ残すfunctionを書けます。 functionの引数へ明示的なpolymorphic typeを付ける構文も加わりました。 以前はfunctorや補助recordへ分離していた処理を、通常のfunctionに近い呼び出しで表せます。 同時に、標準libraryの文字列処理、整数演算、並行初期化、compilerの再配置にも変更があります(5.5.0の変更履歴)。

moduleが持つ型を、functionの型へ残す

値をまとめる処理は、初期値と結合operationを引数に取れば、整数と文字列の両方に使えます。 ただし、初期値の型とoperationの引数型は一致する必要があります。 OCamlのmoduleなら、型t、初期値empty、結合するappendを同じsignatureへまとめられます。 5.5のmodule-dependent functionでは、受け取ったmoduleのtを、後続の値の型として使えます(module-dependent functionの仕様)。

module type Monoid = sig
  type t
  val empty : t
  val append : t -> t -> t
end

let combine (module M : Monoid) (values : M.t list) : M.t =
  List.fold_left M.append M.empty values

combineの最初の引数は、Monoidに合うmoduleです。 次のvaluesは、渡したmoduleのM.tを要素とするlistになります。 返り値も同じM.tです。 moduleが提供するoperationだけを取り出すのではなく、operationと値の型の関係を、functionの公開型へ保持しています。

たとえば整数の加算と文字列の連結は、同じsignatureの別実装にできます。 整数側ではt = int、文字列側ではt = stringを使います。 emptyは加算なら0、連結なら空文字列です。 appendを同じ名前で公開していても、渡せる値の型はmoduleごとに異なります。

module Sum = struct
  type t = int
  let empty = 0
  let append = Int.add
end

module Text = struct
  type t = string
  let empty = ""
  let append = String.cat
end

let total = combine (module Sum) [1; 2; 3]          (* => 6 *)
let joined = combine (module Text) ["a"; "b"; "c"] (* => "abc" *)

Sumを渡した呼び出しでは、M.tをSum.t、さらにintへ対応させています。 Textの呼び出しでは、同じ位置がstringになります。 functionのbodyを2個書くことなく、moduleの型とoperationを組にしたまま呼び出せています。

Sumのtはint、Textのtはstringで、同じcombineの入力listと返り値の型へ反映する図
Sumのtはint、Textのtはstringで、同じcombineの入力listと返り値の型へ反映する図

first-class moduleとの違いは、返り値側の依存

first-class module自体は5.5の新機能ではありません。 以前から、(module S)型の値へmoduleを包み、listやhash tableへ保存できました。 5.5で加わった型は、(module M : S) -> tのようにmodule parameterへ名前を付け、右辺のtから参照する形です。 単にmoduleを値として渡せる性質へ、型の依存を追加しています(first-class moduleと依存する型の関係)。

以前のfirst-class moduleを使う書き方では、M.tをfunctionの外へ逃がさない条件が必要でした。 たとえば局所的に取り出したmoduleの型を、通常の返り値へ無条件に露出できません。 呼び出し元から正体を参照できない型が、返り値だけに残ってしまうためです。 5.5の型では、最初のmodule parameterを公開型で束縛し、後続の型が参照する対象を明示できます。

単純な型なら、従来も(type a)とwith type t = aで関係を宣言できました。 M.tを局所型aへ等置し、listの要素と返り値をaとして書く方法です。 5.5では、型の関係を宣言するためだけの局所型と等式を減らせます。 さらに、型parameterを取る'a M.tにも依存できるため、単純なtの書き換えだけでは届かなかった抽象化を扱えます(modular explicitsの実装PR #13275)。

'a M.tを使う例は、任意のmonadを渡してlistを順に処理するfunctionです。 signatureへtype 'a t、return、bindをまとめれば、結果を包む型constructorをmodule側で選べます。 後続のcallbackは'a -> 'b M.t、最終結果は'b list M.tになります。 M.tは要素型を受け取る型constructorなので、(type a)だけで全体を置き換える構成とは異なります。

functorでも、moduleの型に依存する処理を表せます。 ただしfunctorの適用結果はmoduleなので、functionを使うために結果moduleを束縛する段階が増えます。 返す内容が少数のfunctionだけなら、module-dependent functionでは呼び出しの式へ直接moduleを渡せます。 新しい型や複数の公開値をまとめて生成する用途では、functorが持つmodule単位の結果も引き続き表現上の違いとして残ります。

実行時に選んだpackageを、無条件には渡せない

module-dependent functionへは、staticなmodule argumentを渡します。 combine (module Sum)では、適用した場所でSumというmoduleを参照できます。 一方、(module Monoid)型へ包んで変数に保存すると、packageの内部型を隠した状態になります。 同じ実装を包んでいても、裸のpackage変数を渡す操作と、module式を渡す操作は型検査の条件が異なります。

let chosen : (module Monoid) = (module Sum)
let result = combine chosen [1; 2; 3]

5.5.0では、当該呼び出しは型検査でerrorになります。 診断は、期待する通常の(module Monoid) -> 'aと、依存する(module M : Monoid) -> M.t list -> M.tを示します。 後者のMがscopeの外へ逃げるため、staticなmodule argumentが必要です。 packageを選ぶruntimeの処理だけで、隠れた型を後続の整数listへ一致させられるわけではありません。

実行時の選択が必要なら、packageに残す型の等式を先に決める方法があります。 たとえば(module Monoid with type t = int)なら、選ぶ実装のtを整数へ固定できます。 packageを局所moduleとして取り出し、combine (module M)へ渡せば、返り値のM.tをintへ簡約できます。 返り値へ局所moduleの名前を残さず、公開されている整数型へ戻せるためです(dynamic selectionの制約と回避例)。

依存が残る型constructorでは、同じ方法で単純に等式を除去できない場合があります。 任意の'a M.tへ依存するmonadの例では、型parameterを取らないtだけの例より制約が強くなります。 module-dependent functionが追加された結果、すべてのmodule選択が実行時の値へ自由に移せる規則になったわけではありません。 型の依存を利用する呼び出しは、依存先のmoduleを適用位置で確定する必要があります。

複数引数をまとめて適用する式にも、型情報を伝える境界があります。 公式manualのFun.id sort (module ISetIncr)は、最初にFun.idの型を使って引数列を検査するため失敗します。 (Fun.id sort) (module ISetIncr)と括弧を加えれば、内側の適用で得た依存する型を、外側のmodule argumentの検査へ使えます。 値の計算順序を変えるための括弧ではなく、型検査へ既知のfunction型を渡す単位を変えています。

polymorphicなfunctionを、引数として受ける

通常のOCamlのfunction parameterは、body内で使う型が1個へ統一されます。 呼び出し元のfunctionが多くの型で使えても、受け取る側が毎回異なる型で呼べるとは限りません。 5.5では、parameterへ'a.を含む明示的なannotationを付け、受け取るfunctionが任意の'aへ対応する条件を表せます。 body内で整数とbooleanへ別々に適用できます(polymorphic parametersの実装PR #13806)。

let use (identity : 'a. 'a -> 'a) : int * bool =
  identity 41, identity true

let result = use Fun.id (* => (41, true) *)

identity 41では'aをintへ、identity trueではboolへ具体化しています。 受け取ったfunctionの型を1回だけint -> intへ固定せず、使用箇所ごとに具体化する条件を、parameterの型へ書いています。 呼び出し元には、整数専用のfunctionではなく、任意の型の値を同じ型で返せるfunctionが必要です。

annotationを外したlet use identity = identity 41, identity trueでは、最初の呼び出しがidentityの型を整数へ固定します。 5.5.0では、boolean literalの位置で整数型を要求する型の不一致になります。 最初の適用によって、同じparameterの入力型がintへ固定されるためです。 新機能の追加後も、annotationのないparameterを無条件にhigher-rankな型へ推論する動作にはなっていません。

型変数を明示していない普通の'a -> 'aと、'a. 'a -> 'aは、受け取る側で約束する範囲が異なります。 前者の'aは周囲の型検査に従って1個の型へ決まります。 後者は、parameter自身が任意の型について成立するという条件です。 整数を受け取って整数を返すfunctionは前者の具体例になれますが、booleanでも呼ばれる後者の条件は満たしません。

GADTのfieldごとに、異なる型の値を返すcallback

実装PRでは、型付きfieldの記述からrecordを作る例を扱っています。 fieldのGADTは、文字列のfieldを指定すればstring field、整数ならint fieldになります。 recordを構築するcallbackには、'a field -> 'aという対応が必要です。 同じ構築処理の中で、文字列fieldと整数fieldの両方をcallbackへ渡すため、callback自体をpolymorphicに受け取ります。

以前もpolymorphicなrecord fieldへcallbackを入れれば、同じ型の関係を表せました。 たとえば専用recordのfieldをget : 'a. 'a field -> 'aにする方法です。 ただしcallbackを渡すためだけのrecord型と、呼び出し側で包む式が増えます。 5.5ではfunction parameterへ型を直接付け、callbackを専用の値へ包む段階を減らせます(型付きfieldの具体例と従来の代替)。

parameterのannotationは、function全体の型から伝える書き方も可能です。 たとえばuse : ('a. 'a -> 'a) -> int * boolのように、公開する型へ条件を置けます。 期待する型がapplicationから伝わる場合もありますが、型情報のない境界ではmonomorphicなparameterと仮定します。 単にcallbackのbodyが複数の型で動くという理由だけで、必要なquantifierの位置をすべて自動で補うわけではありません。

普通のapplyへ渡した場合の型変数のscope

通常の型を持つwrapperを挟むと、polymorphic parameterの量化を維持できない場合があります。 let apply function_ argument = function_ argumentは、通常の('a -> 'b) -> 'a -> 'bとして推論されます。 apply use Fun.idは、useのpolymorphic parameterを普通の型変数へ埋め込もうとするため、型検査でerrorになります。 診断は、universal variableがscopeを逃げることを示しています。

use Fun.idが通り、apply use Fun.idが通らない理由は、functionの実行内容の差ではありません。 applyの型が、受け取るfunctionの引数へhigher-rankな型を許す契約を持たないためです。 公開APIへpolymorphic parameterを加えた場合、経由するwrapperの型も同じ条件を表す必要があります。 普通の型変数1個に、任意のpolymorphic typeを代入できるimpredicativeな型体系へ変わったわけではありません。

module-dependent functionとpolymorphic parameterは、同じ依存を別名で呼ぶ機能でもありません。 前者はmoduleのabstract typeや型constructorを後続の型へ残します。 後者は受け取るfunctionを、使用位置ごとに異なる型へ具体化できる条件です。 module側で提供する型の族と、callback側で量化する型変数を、別の境界へ置ける構成になっています。

local definitionをtypeやeffectへ広げる

5.5では、let module、let exception、let openに限られていたlocal definitionを広げています。 let type ... in ...なら、function内部の処理だけに使う型を、moduleで包まずに定義できます。 型のconstructorも、後続の式を検査するscopeへ加わります。 ColdとWarmを持つ型も、function内部だけへ定義できます(local definitionの仕様)。

let describe () =
  let type state = Cold | Warm in
  let current = Warm in
  match current with Cold -> "cold" | Warm -> "warm"

let result = describe () (* => "warm" *)

stateの値はfunction内部で分岐に使いますが、返り値は普通のstringです。 返り値へ局所型の名前を持ち出さず、外から参照できる型の値へ変換しています。 既存のlet module Local = struct type state = ... end in ...を、型の定義だけが必要な用途で短く書ける構成です。

effectのconstructorをlocalに追加するlet type Effect.t += Yield in ...も、新しい対象です。 effectを宣言する位置を処理の近くへ置けるため、module全体で公開するconstructorと、局所的なhandlerで使うconstructorを分けられます。 handlerの意味やcontinuationの使用規則を変える機能ではなく、宣言できるstructure itemとscopeの変更です。 effect handlerを定義する構文の追加と、local definitionの一般化は公開された版が異なります(local definitionを一般化したPR #14040)。

PPXが読むattributeの位置には、互換性へ関わる変更もあります。 let module%foo[@bar] ... in ...のattributeは、内側のstructure itemのASTへ付きます。 外側のlet式へ付けたい場合は、let%foo[@bar] module ... in ...と書きます。 let exceptionとlet openも同じ扱いです。 sourceの表記を保っていても、PPXの読む対象nodeは変わり得ます(attributeの付与先を変えるPR #14009)。

型をlocalに宣言できることと、local typeを返り値から参照できることは独立しています。 describeが文字列へ変換して返す構成なら、返り値の型にstateを残しません。 局所のconstructorを返すAPIを作りたい場合には、外部から識別できる型の定義やpackageの形が必要です。 scopeを小さくする構文は、型を外へ公開する契約を省略する仕組みにはなりません。

external typeは、abstract typeの区別へ情報を加える

type t = external "name"は、OCamlの通常のdatatypeだけでは表さない外部の値を扱う型です。 FFIを通じてC libraryが提供する値などへ、外部型の名前を付けます。 名前が異なるexternal typeは、型検査で異なる型として区別できます。 一方、同じ名前を付けただけで、別々に宣言したexternal typeが自動で等しくなるわけではありません(external typeの仕様)。

type left = external "report.left"
type right = external "report.right"

let impossible (proof : (left, right) Type.eq) =
  match proof with _ -> .

Type.eqは、2個の型が等しいという証拠を表します。 異なる外部名を持つleftとrightなら、等しいというconstructorが成立する場合を排除できます。 _ -> .は到達できないcaseを指定するrefutation caseで、異なるexternal type間の等しさを排除します。 値をunsafe castした実験ではなく、型の非互換性を使ってcaseを除外する型検査の例です。

外部名を外し、単にtype leftとtype rightへ置き換えた宣言では、同じrefutation caseを受け付けません。 当該caseは到達不能だと判定できず、到達し得るconstructorとしてEqualを表示します。 abstract typeは具体的な定義を隠しているため、宣言の名前が違うだけで、任意のmoduleの境界を越えて不等だとは断定できません。 5.5では、現在のmoduleにあるabstract typeを特別に区別していた従来の挙動も変更されています(external type導入のPR #13712)。

built-inのfloatやstringも、異なる外部名を持つ型として扱います。 GADTのindexに使った場合、floatとstringが一致するconstructorを除外できる根拠になります。 外部名は、表示するためだけのaliasではなく、型同士を区別する検査へ情報を加えます。 foreign functionを自動生成する機能ではないため、値を生成・解放するFFIの宣言は別に必要です。

型parameterのvarianceは、abstract typeと同様に宣言側で指定します。 指定しなければinvariantです。 external typeの型constructorはinjectiveなので、同じ外部型constructorを適用した結果が等しければ、引数型の等しさも検査へ使えます。 型を区別する外部名、variance、injectivityは、GADTやmoduleの境界で利用できる情報の異なる部分です。

以前のrefutation caseが失敗する場合、warningを無条件に無効化するだけでは到達不能の根拠を回復できません。 外部の値を表す型ならexternal type、OCamlのconstructorを使う型なら公開するdatatypeの構造へ、必要な区別を持たせます。 型の不等性を使って省略していたbranchは、compilerが不等性を知る宣言を持つかが移行条件になります。 同じ見た目のphantom labelでも、abstract typeの名前だけに依存していた構成は、5.5の型検査で異なる結果になり得ます。

文字列APIは、byte位置と非重複の一致を扱う

5.5のStringには、substringの検索、分割、置換が加わりました。 find_firstやfind_lastは位置を返し、split_firstやsplit_lastは左右の文字列を返します。 split_allとrsplit_allは、一致したseparatorを使って全体を分割します。 replace_first、replace_last、replace_allは、一致したsubstringを指定した文字列へ置き換えます(Stringの5.5 API)。

APIが使う位置は、Unicodeの文字数ではなくbyte位置です。 String.lengthはbyte数で、indexは0から長さ未満、positionは末尾を含む0から長さまでになります。 検索結果の値をUnicodeのcode point番号と扱うと、UTF-8で複数byteを使う文字の後から位置がずれます。 空文字列の一致では末尾のpositionも有効なので、返った位置が常にString.getへ渡せるindexとは限りません。

separatorを"::"とする分割では、空の要素を残すか選べます。

let parts = String.split_all ~sep:"::" "::a::::b::"
(* => [""; "a"; ""; "b"; ""] *)

let nonempty = String.split_all ~sep:"::" ~drop:String.is_empty "::a::::b::"
(* => ["a"; "b"] *)

先頭と末尾のseparator、および隣接するseparatorの間を、空の要素として保持しています。 ~drop:String.is_emptyは、同じ分割結果から空の文字列を除きます。

空の要素を落とさない版には、同じseparatorで連結すると元の文字列へ戻る性質があります。 前述の入力でも、空の要素を保つことで、先頭、末尾、連続したseparatorを復元できます。 dropを指定して要素を取り除いた版は、同じ復元条件を保持しません。 設定ファイルの空fieldを保持したい処理と、空白区切りのtokenだけが必要な処理では、同じ分割APIでも選ぶoptionが異なります。

重なる候補では、検索方向によって分割位置が変わる

split_allは、非重複の一致を前から選びます。 入力"aaa"とseparator"aa"では、先頭の2 byteを一致させ、残りの"a"を右側へ残します。 同じ入力をrsplit_allへ渡すと、末尾側の"aa"を選ぶため、["a"; ""]になります。

let forward = String.split_all ~sep:"aa" "aaa"  (* => [""; "a"] *)
let reverse = String.rsplit_all ~sep:"aa" "aaa" (* => ["a"; ""] *)

両方の一致候補を採用すると、中央のaを2回消費してしまいます。 非重複という条件は、候補が重なる場合に、検索方向によって採用する位置が変わる理由です。 返すlistの方向を単純に反転するAPIとして考えると、前述の入力では結果を説明できません。 後ろから一致を選んでも、返すsubstringは元の文字列を再構成する順序を保っています。

置換も、先頭側の非重複の一致を選びます。

let replaced = String.replace_all ~sub:"aa" ~by:"X" "aaa" (* => "Xa" *)
let inserted = String.replace_all ~sub:"" ~by:"-" "ab"   (* => "-a-b-" *)

重なる2個の候補を両方置換して"XX"にする処理ではありません。 検索位置と置換対象の消費範囲が、分割APIと同じ非重複の条件へ接続しています。

空のsubstringには、すべてのpositionへ一致する意味があります。 文字の間だけでなく、先頭と末尾にも置換文字列を挿入します。 空のseparatorによる分割も、先頭と末尾の空要素を含むため、空文字列を通常の不一致として扱う実装とは結果が異なります。

find_allとrfind_allは、結果の位置をlistへ集めるAPIだけではありません。 一致位置とaccumulatorを受けるfunctionでfoldする形なので、一致数や位置の集約値を直接計算できます。 検索結果を全件保存してから数える中間listを、必ず作る構成にはなりません。 返り値をoptionalな最初の位置にしたい場合と、全一致を処理したい場合で、公開するAPIの型も分かれています。

同じsubstringで多数の文字列を検索するときは、~subや~sepを部分適用できます。 公式APIは、固定したsubstringの前処理を再利用する使い方を案内しています。 単発の呼び出しを短くする記法だけでなく、同じ検索条件を使う処理のまとまりを表せます。

長さで切るAPIと、条件で切るAPI

take_first、take_lastは、指定したbyte数を先頭または末尾から取り出します。 drop_first、drop_lastは、対応するbyte数を取り除きます。 cut_first、cut_lastは、保持する部分と残りをtupleで返します。 長さが文字列より大きい場合や0以下の場合の結果も定義され、単なるString.subの引数作成とは異なります(大きさに基づく分割)。

take_first n sは、nが長さ以上ならs全体、0以下なら空文字列です。 drop_first n sは、nが長さ以上なら空文字列、0以下ならsを返します。 String.subなら不正な範囲として例外になる入力も、取り出す量を端へ制限する意味で扱います。 負の長さを不正入力として拒否したいAPIから置き換える場合は、入力の拒否条件まで同じにはなりません。

take_first_whileは、predicateを満たす連続した先頭部分を返します。 全体から条件に合うbyteを抜き出すfilterではないため、途中で条件を満たさないbyteへ到達すると終了します。 drop_last_whileなら、末尾の連続した該当部分を除きます。 先頭の数字、末尾の区切り文字、接頭部の空白など、境界へ連続する部分を扱うAPIです。

predicateが受け取る値もchar、すなわちbyteです。 UTF-8の任意の1文字をpredicateへ渡す構成にはならないため、Unicodeの文字種を判定する処理を直接代入できません。 ASCIIの区切りを検出する処理と、Unicodeのcode pointをdecodeする処理では、入力単位が異なります。 5.5の追加は、既存のStringのbyte列という契約を維持して、切り分けるoperationを増やしています。

整数の除算は、丸める方向をAPIで選ぶ

5.5では、Int、Int32、Int64、Nativeintへ、丸め方を指定する除算が加わりました。 fdivは負の無限大へ切り下げ、cdivは正の無限大へ切り上げます。 edivとeremはEuclidean divisionの商と余りで、0でない除数について余りが非負になります。 従来のdivとremは、商を0へ向けて丸める処理を維持します(Intの除算API)。

入力-7と除数3では、丸める方向によって商と余りが変わります。

let truncated = (Int.div (-7) 3, Int.rem (-7) 3) (* => (-2, -1) *)
let directed = (Int.fdiv (-7) 3, Int.cdiv (-7) 3) (* => (-3, -2) *)
let euclidean = (Int.ediv (-7) 3, Int.erem (-7) 3) (* => (-3, 2) *)

edivは-3、eremは2なので、-7 = (-3) * 3 + 2と、非負の余りという条件が同時に成り立ちます。

正の入力では、0へ向ける除算とfloor divisionの結果が一致するため、負の入力で現れる差を区別できません。 負の座標を固定幅のbucketへ分ける用途では、0付近の区切りが変わります。 たとえば幅3で-1を扱うと、0へ向けた商は0ですが、floorの商は-1です。 正負をまたぐ同じ幅の区間を選ぶ処理には、丸め方が分割規則の一部になります。

Euclideanな余りは、常に商をfloorへ丸めれば得られるとは限りません。 除数が正ならedivはfloor側、負ならceil側へ丸めます。 余りを0以上に保つ条件が基準なので、除数の符号によって商の丸める方向が変わります。 単にmodの名前だけをeremへ変更する移行では、負の入力で返す値の契約が変わります。

除数0は、新しい除算でもDivision_by_zeroです。 丸め方の追加によって、整数の表現幅が無制限になったわけではありません。 Intの演算はSys.int_sizeの幅に従うため、数式として成立する任意精度の計算と、machine integerの範囲を同一視できません。 新しいAPIが選ぶのは商と余りの規則であり、型が表せる整数全体の範囲は使用したmoduleで決まります。

bitの個数と、値を表すための幅

bitを数えるAPIも、同じ整数moduleへ追加されています。 popcountは1のbit数、leading_zerosとtrailing_zerosは先頭と末尾へ連続する0の数です。 unsigned_bitsizeとsigned_bitsizeは、値をunsignedまたはtwo's complementのsignedな整数として表すための最小幅です。 bitの個数と必要な表現幅は、同じ値から得る別の性質です(bit countingの定義)。

15に対するpopcountとunsigned_bitsizeの返り値は、ともに4です。

let counts = (Int.popcount 15, Int.unsigned_bitsize 15, Int.signed_bitsize 15)
(* => (4, 4, 5) *)

binary表現の1111は、1を4個持ち、unsignedなら4 bitで表せます。 一方、signed_bitsizeは5です。 正の15をtwo's complementで表すには、負の数と区別する先頭の0が必要なので、4 bitの1111だけでは足りません。

負数のpopcountは、two's complement表現のbitを数えます。 数学上の負の整数へ、符号を付けた絶対値のbit数を数えるAPIではありません。 IntとInt64で表現幅が異なれば、負数の上位へ並ぶ1の数にも差が生じます。 binary formatを扱う実装では、payloadの幅をSys.int_sizeへ暗黙に合わせるか、固定幅のmoduleを使うかが、結果の条件になります。

leading_sign_bitsは、先頭の符号bitを除いて、同じ符号へ連続する上位bitを数えます。 正数なら先頭の0、負数なら先頭の1が対象です。 signed_bitsizeと足すと表現幅になる関係があるため、符号を維持して幅を縮める処理へ使えます。 0のtrailing_zerosは幅全体で、無限個の0とする数学的な定義ではなく、有限幅のAPIとして値が決まります。

Optionとcollectionの追加は、値の有無を組み合わせる

Option.productは、両方に値がある場合だけtupleを返します。 片方がNoneなら、返り値もNoneです。 Option.blendは異なり、片方だけに値があれば値のある側を返し、両方に値がある場合だけ指定したfunctionで結合します。 同じ2個のoptionを受け取っても、必要とする値の存在条件が異なります(Optionの5.5 API)。

たとえば2個の任意設定を組み合わせる処理で、両方を必須にするならproductです。 片側の設定だけでも採用し、競合した値を統合したいならblendになります。 blendへ渡すfunctionは同じ型の2値を結合するので、異なる型をtupleへまとめるproductとはsignatureも異なります。 値を失敗として伝播する条件と、欠けた側を補って結合する条件を、別のfunctionへ分けています。

Option.for_allとOption.existsは、空のoptionへ異なるbooleanを返します。 同じpredicateでも、Noneに対する結果は異なります。

let universal = Option.for_all (fun n -> n > 0) None (* => true *)
let existential = Option.exists (fun n -> n > 0) None (* => false *)

for_allの条件を破る要素は空のoptionに存在せず、existsの条件を満たす要素も存在しないためです。 存在の検査を意図した処理へfor_allを使うと、値がない入力まで成功させる意味になります。

Option.Syntaxにはbinding operatorが加わりました。 let*で後続のoptionを返す計算をつなぎ、and*などで値の有無を組み合わせる書き方を、moduleのAPIとして使えます。 operatorの見た目だけを導入するのではなく、Noneを後続へ伝えるoperationを選んでいます。 recordのfield名のような普通の名前とは異なり、使用するscopeでoperatorの意味を確定させる構成です。

List.split_mapは、各要素から2値を返すfunctionを適用し、結果を2個のlistへ分けます。 元のlistを2回mapする代わりに、同じ要素から対応する2値を作る処理をまとめられます。 List.filter_mapiでは、indexを受ける変換がNoneを返せば要素を落とし、Someなら変換後の値を残します。 要素の選択と変換を別の走査へ分離していた処理を、単一のcallbackの返り値へ表す追加です(Listの5.5 API)。

Hashtbl.find_and_replaceは、以前の値をoptionで返しながら置き換えます。 find_and_removeは、取り出した値を返しながら削除するAPIです。 以前の値の存在を知りたい処理で、検索と更新を別々の呼び出しへ分ける必要を減らします。 通常のHashtblへの同時アクセスを自動で同期するAPIではないため、複数domainから共有する構成の同期条件は別に残ります(Hashtblの追加API)。

Set.SとMap.Sにはis_singletonが加わりました。 要素数がちょうど1個である条件を、cardinalityの整数を取り出さずに検査できます。 Dynarray.rev_iterとrev_iteriは、後ろからの走査を公開します。 既存の抽象型へ、新しい走査や条件判定の入口を増やしています(標準libraryの追加記録)。

Formatは、異なる型の引数を1個の引数列へまとめる

5.5では、Format.lprintfやFormat.lfprintfなどが加わりました。 従来のprintfのように引数を個別に適用する代わりに、異なる型の値を持つ専用の引数列へまとめます。 文字列と整数の引数列を、同じformat stringへ対応付けられます。

let () = Format.lprintf "%s=%d@." ["heterogeneous"; 42]
(* => heterogeneous=42 *)

文字列と整数を同じ通常のhomogeneousなlistへ入れる規則になったわけではありません(heterogeneous argument listの仕様)。

引数列は、format stringから得る型と対応したArgs.tです。 先頭の%sには文字列、次の%dには整数という順序を、型検査で照合できます。 通常のlistが全要素の型を揃える条件と異なり、位置ごとに要求する型を持つ構造です。 角括弧の見た目が同じでも、期待される型を通じて使用するconstructorが決まっています。

従来のcurriedな書式出力では、引数をまだ渡し終えていない式もfunctionとして成立します。 必要な引数の不足を、呼び出しの完了位置として判定するには、周囲で期待する型も関係します。 lprintfでは引数列の終端が構文に現れるため、format stringと引数列の対応を同じ境界で検査できます。 指定子の不足や余分な引数は、長いapplicationの末尾より、引数列の不一致として対応付けられます。

lfprintfは出力先のformatterを受け、lasprintfは文字列を返します。 ldprintfはformatterを後で受け取る形へ整形処理を遅らせます。 引数列を1個へまとめる追加なので、継続を渡して引数の終端を知る専用のvariantを使わずに、渡す値の区切りを表せます。 formatterのbox、break hint、flushの規則は、対応する既存APIと同じです。

@.はFormatの整形処理を終えて改行する指定です。 通常の文字列の\nだけを入れる操作とは、pendingな出力をflushする条件が異なります。 新しいheterogeneousな引数列が追加されても、Formatが出力をqueueへ置いて行分割を判断する仕組みは維持されます。 @.は改行とflushを行い、bufferに残る文字列も出力します。

複数domainから共有する初期化をLazy.Mutexedで守る

Lazy.Mutexedは、複数domainやthreadから使えるmutex付きの遅延計算です。 最初のforceで初期化を実行し、初期化中に別のforceが来た場合はmutexでblockします。 初期化が値を返した後は、同じ値を再利用します。 例外を投げた場合も、以降のforceで同じ例外を投げる契約です(Lazy.MutexedのAPI)。

let calls = Atomic.make 0
let delayed = Lazy.Mutexed.from_fun (fun () ->
  Atomic.incr calls;
  42)

let worker = Domain.spawn (fun () -> Lazy.Mutexed.force delayed)
let first = Lazy.Mutexed.force delayed (* => 42 *)
let second = Domain.join worker       (* => 42 *)
let count = Atomic.get calls          (* => 1 *)

2個のdomainが同じ遅延計算を参照していても、bodyを実行した回数は1回です。 counterの加算はAtomic.incrで行い、共有する整数へのdata raceを防ぎます。

普通のLazy.tのforceは、同じ値へ複数domainから並行して到達する用途のmutexを自動で提供しません。 optionalな設定読み込みなどをlibraryの初回利用まで遅らせる処理では、呼び出すdomainを限定できない場合があります。 Lazy.Mutexedは、初期化を実行する側と結果を利用する側の同期を、遅延計算の容器へ含めます。 初期化のfunctionが捕捉したほかの共有状態まで、すべてmutexで保護する意味ではありません。

fiberへyieldする初期化は、同じthreadでmutexへ再入する

Lazy.Mutexedが使うのは、標準libraryのMutexです。 user-levelのfiberが初期化中にyieldし、同じthread上の別fiberが同じ遅延計算をforceすると、同じmutexへ再入する場合があります。 別domainで初期化の終了を待つ例と異なり、同じthreadの制御が初期化の途中へ重なっています。 公式APIは、同じthreadで再びforceした場合にSys_errorを投げる条件を説明しています。

再帰的に自分自身をforceする場合も、同じthreadのmutex再入になります。 通常のLazy.tが持つLazy.Undefinedの経路と、同じ例外の契約ではありません。 fiberを内部で使う計算なら、schedulerが提供するblockingやmaskingの仕組みと整合する必要があります。 Lazy.Mutexedの初期化bodyには、threadのmutexと整合する計算が必要です。

同じmoduleのfrom_valは、初期化済みの値から遅延計算を作ります。 is_valは、force済みで結果が値になっている場合にtrueを返します。 例外で終了した計算を、成功した値と同じ状態として報告するAPIではありません。 遅延する初期化の有無、成功した値の有無、複数domainの競合を、公開するoperationで別に扱っています。

GCは、回収する時期と走査する範囲を変える

major GCには、cycleの最初にsweepだけを進めるphaseが入りました。 以前のcycleで到達不能になったblockを、再利用できる状態へ移すまでの遅れを短くする変更です。 実装PRでは、到達不能になってから再び割り当てられるまでのlatent garbage delayを対象にしています。 heapが同じ大きさでも、使用済みの領域を再利用できる時期が遅ければ、新しい領域への要求が増えるためです(sweep-only phaseのPR #13580)。

markingは、rootから到達できるobjectを追って、生存するblockへ印を付ける処理です。 sweepingは、生存しないblockの領域を、再割り当てできる側へ回します。 5.5の追加では、cycleの最初から両方を同じ進行条件で扱う代わりに、先にsweepingを進める期間を置きます。 markingとroot markingはcycleの途中から始まるため、新しく割り当てるblockへ付ける色もphaseに応じて切り替わります。

割り当て直後のblockの扱いを変える理由は、実行中のprogramが作る参照と、collectorが追う参照を整合させるためです。 cycleの途中で作った値を、生存の記録がない古い不要blockと同じ条件で回収すると、programが使う値を失います。 実装PRでは、UNMARKEDで割り当てる段階からMARKEDで割り当てる段階へ移る構成を説明しています。 単にcollectionを多く呼ぶ変更ではなく、cycleの中で作業を配置する順序と、新規割り当ての状態を合わせています。

小さいheapには、major GCのIdle phaseも加わりました。 programの起動直後や、強制したmajor collectionの後から、毎回同じ量の回収作業を開始する必要を減らします。 起動時の小さい生存集合と、継続して大量に割り当てる状態を、同じ開始条件へ押し込まない構成です。 Idleから開始し、full major GCの後もIdleへ戻す変更が実装されています(Idle phaseのPR #14365)。

深いstackを持つprogramのminor collection

ARM64、POWER、RISC-Vには、stack frameのgenerational scanningが入りました。 minor GCが若いobjectへの参照を見つける際、深いcall stackを毎回同じ量だけ走査する負担を減らします。 若い世代へ関係する部分を追うcollectorに、stack側の世代に関する情報を使う変更です。 heapのyoung generationだけでなく、heapを参照するroot側の走査量も回収時間へ関わります(stack scanningのPR #13574)。

深い再帰を使うprogramでstackが大きくなる場合、heapの割り当て量が同程度でも、rootを探す作業は増え得ます。 generational scanningは、若いobjectを回収するたびに古いstack領域を全走査する負担へ作用します。 sourceの再帰をtail callへ変換する最適化とは異なり、存在するstack frameをcollectorが読む方法の変更です。 5.5の記録で対象に挙がったarchitectureに関する追加なので、すべてのbackendへ新しい走査方式が同時に入ったという範囲ではありません。

Gc.statのrecordには、live_stacks_wordsが加わりました。 heap外へ確保したstack fragmentとmetadata、cache内のstack fragmentを含む量をword単位で表します。 現在のstackの大きさを表すstack_sizeと、stackのために確保した領域全体は同じ量ではありません。 停止中のcontinuationや再利用するfragmentも関係するため、heapのlive objectだけからstack用の領域を推測する必要を減らします(GC統計のfield定義)。

runtime eventsには、minor heapへ割り当てたword数と、minor GCでpromoteしたword数のcounterが追加されました。 対応するbyte単位のcounterもあり、eventを出したdomainの量として扱います。 全domainの合計へ1個のdomainのcounterを足すと、同じdomainの値を二重に数えます。 5.5.1では、EV_C_MINOR_ALLOCATED_WORDSがheaderを含むword数を報告する修正も入りました(runtime counterの追加と5.5.1の単位修正)。

GC parameterを名前で指定するGc.Tweak

Gc.Tweakは、追加するGC parameterへ名前を付ける仕組みです。 従来のOCAMLRUNPARAMの1文字の設定に加え、Xfoo=123のような名前付きの指定を扱います。 設定できるparameterを追加した場合、runtimeの設定文字列と、Gc側のAPIから同じparameterへ接続できます。 公開されるSys.runtime_parametersにも、設定内容が現れます(named parameterのPR #14367)。

名前を付ける変更は、すべての内部定数を安定した一般用途の設定へ昇格する意味ではありません。 実装PRでは、mark stackのpruning thresholdを、仕組みを使う具体的なparameterとして公開しています。 固定した内部値を変更可能にする入口と、一般的なGc.controlのfieldとして契約する入口には、対象となる設定の違いがあります。 同じ文字列を使った起動でも、存在しないparameterを使える前提へ広げず、compilerとruntimeの版へ設定名を結び付ける必要があります。

Gc.Memprofには、sampling中かを調べるis_samplingが加わりました。 current domainのprofileを、全domainのsamplingを止めずに置き換える処理も可能になります。 program全体を単一のprofileの開始・終了へ固定していた境界を、domainごとの操作へ分ける変更です。 sampleの集約先を切り替える操作と、heapのobjectを回収するGC自体の操作は、異なる状態を扱います(Memprofの変更)。

domainとFFIに関する変更

Domain.count ()は、作成済みで終了していないdomainの数を返します。 最初のdomainも数に含みます。 recommended_domain_count ()が推奨する最大数に関する情報なのに対し、countは実際に生きているdomainの個数です。 CPU数に関する推奨値と、programが保持する並列実行単位を、別のAPIから読めます(DomainのAPI)。

domainが例外で終了した場合には、Domain.joinが元のbacktraceを保持して例外を投げ直します。 別domainの失敗を受け取った場所だけを記録する構成より、失敗した処理の呼び出し位置を追えます。 親domainでbacktraceの記録を有効にしていた場合、spawnしたdomainも記録するようになります。 並列処理を起動した側の設定と、例外を回収する側の記録が、同じ調査経路へ接続する変更です(backtraceを保持するPR #14363、設定継承のPR #14416)。

C側から作成したthreadを登録するAPIには、caml_c_thread_register_in_domainが加わりました。 domainのunique IDを渡し、domain 0以外へC threadを登録できます。 登録先のdomainは、呼び出し時に動作している必要があります。 単に終了済みのIDを保存しておき、後から同じ番号の実行領域があると仮定する操作ではありません(C thread登録のPR #14275)。

通常のcaml_c_thread_registerは、domain 0へ登録する入口です。 新しいAPIは、外部libraryが作るthreadを、意図したdomainへ結び付ける情報を受け取ります。 OCamlのruntimeへ登録することと、外部library側の共有状態を保護することは別なので、callbackで触る値の所有と同期はFFI側の契約に残ります。 5.5では、domainの作成やthreadの登録中にまれなerrorが起きる経路のdeadlockやleakにも修正が入っています。

Windowsでは、並行処理のprimitiveをWinAPIで実装する構成になりました。 Winpthreadsへの依存を外し、platformの同期APIへ直接接続します。 同時にUnix.kill pid Sys.sigkillの終了codeは、従来の0からERROR_PROCESS_ABORTEDの1067へ変わりました。 processの終了理由を終了codeから分類するtoolでは、正常終了と同じ0を期待した判定が変更対象です(WindowsのruntimeとUnix APIの変更)。

compilerをinstall先から移せる構成

5.5のcompilerはrelocatableになりました。 binaryと標準libraryの配置関係を維持してinstall treeを移せば、元の絶対pathだけへ依存せず動作できます。 単にocamlcのfileを別のdirectoryへコピーする話ではなく、compiler、runtime、標準libraryが相互の位置を求める仕組みの変更です。 標準libraryの場所を、binary自身の位置から相対的に探す構成を持ちます(relocatable compilerのPR #14244)。

--with-relative-libdirは、runtimeとcompilerが標準libraryを相対位置から探すためのbuild設定です。 以前のcaml_standard_library_defaultへ埋めた絶対pathだけを前提にしません。 -set-runtime-defaultは、runtimeが使う標準libraryの既定位置を、compilerから上書きするoptionです。 配布するtreeの構成と、生成した実行fileがlibraryを探す設定を、明示的に合わせられます。

Sys.runtime_executableは、実行中のruntimeの完全pathを取得する入口です。 bytecodeのprogramでは、programのfileと、動かしているruntimeのbinaryは同じ対象とは限りません。 runtimeが自分自身の場所を知ることで、相対配置のlibraryを探す処理へ接続できます。 sourceの所在を示すpathと、実行中のruntimeの所在を示すpathを混ぜずに扱うための情報です(runtimeのpathを得るPR #13728)。

ld.confの相対pathと、runtime ID

ld.confの明示的な相対pathは、設定fileのdirectoryを基準に解釈します。 .、..、./...、../...が対象で、起動時のcurrent directoryへ無条件に結び付けません。 既定のld.confも相対pathを使う構成になります。 install treeを移した場合に、移動前の標準libraryの絶対pathを参照し続ける問題を減らす変更です(相対pathの解釈を変えるPR #14243)。

設定fileは、OCAMLLIB、CAMLLIB、標準のlibrary位置にあるld.confを、最初の1個だけでなく複数読み込みます。 CRLFの改行もWindowsとUnixの両方で揃えて処理します。 一方、CAML_LD_LIBRARY_PATHの解釈は変更しません。 相対pathの基準を変える設定fileと、既存の環境変数を、同じ移行規則へ一括で置き換える変更ではありません。

runtimeには、configurationとversionの違いを識別するruntime IDも加わりました。 host tripletと組み合わせ、ocamlrunやruntimeのDLL、stub libraryのfile名を区別します。 同じmachineへ異なるruntime構成をinstallした場合、同じ短いfile名だけを探索して誤ったstubへ到達する条件を減らします。 利用者向けの従来名はsymlinkで保持し、名前のsuffix付与は--disable-suffixingで無効化できます(runtime IDとfile名のPR #14245)。

bytecodeの起動には、-launch-methodと-runtime-searchも加わりました。 前者はinterpreterを起動する方式を指定し、後者は実行fileと同じdirectoryやPATHからruntimeを探すheaderを作ります。 単一のhard-codedなpathだけに固定せず、配布したprogramがruntimeへ到達する経路を選べます。 ocamlobjinfoも、bytecode実行fileが起動するruntimeを、RNTM sectionやshebangから表示できます。

ocamlc -customでlinkした実行fileは、まず実行file自身からbytecodeを読み込みます。 以前のargv[0]を先に試す構成を変え、起動時の引数の綴りだけにfileの所在を委ねません。 実行fileの探索、runtimeの探索、libraryの探索は、同じ配布treeでも別の位置解決です。 5.5のrelocationは、必要なfileの組と相対配置を維持した配布へ対応する変更としてまとまっています。

5.5.1は型検査、thread切り替え、runtimeの統計を修正する

5.5.1には、module-dependent functionとdependentなfirst-class module typeを組み合わせた型検査の修正があります。 moduleが内部にmodule typeを持ち、後続の引数が内部のsignatureへ依存する場合のscope errorが対象です。 5.5.0で型検査を通った事実だけでは、依存するmodule typeを渡す全構成で正しい検査が行われた根拠にはなりません。 修正は、通常の非依存applicationとして扱う過程で失っていたscopeの条件を戻します(型検査のPR #14982)。

Windowsのsysthreadには、runtime lockを保持し続けるregressionの修正が入りました。 threadが自発的にyieldしない場合、preemptiveな切り替えが効かず、同じdomainのほかのthreadが動けなくなる問題です。 明示的なThread.yieldやblocking sectionだけで切り替わる状態を修正します。 domainを複数にする変更とは別に、同じdomain内でsysthreadがruntimeを使う順序へ関わります(thread切り替えのPR #15029)。

stack fragmentの内部cacheには、サイズの上限を付ける修正があります。 再利用のためにstackを保持する仕組みでも、保持する量を制限しなければ、必要な計算が終わった後のmemoryが増え続けます。 5.5.0のstack統計は使用量を返す変更で、5.5.1の修正は実際に保持するstackの量を制限します。 liveな値だけを数えたheapの統計では、cacheの確保領域全体を同じ意味で表せません(stack cacheのPR #14940)。

runtime eventsでは、欠けていたEV_MINOR_EPHE_CLEAN constructorが追加されました。 runtime側は以前からphaseを送っていましたが、OCaml側のvariantに対応するconstructorがありませんでした。 利用側が受け取った値をpattern matchすると、定義した範囲外の値になり、crashする経路がありました。 eventを出すruntimeと、decodeした後の値の型を、同じ集合へ揃える修正です(欠けたphaseのPR #14966)。

parallel buildでは、interface fileを複数のMake ruleが競合して作る経路も修正されています。 記録されるsource pathが変わると、interface digestや後続の生成物、debug情報にも違いが伝播しました。 同じsourceとcompilerを選んでも、並行する生成処理の勝者によって配布物のbyte列が変わる問題です。 5.5.1では、strとunixの対象fileを担当する生成ruleを揃えています(再現可能なbuildのPR #14989)。

5.5.2で修正された入力と最適化

5.5.2は、使わない結果を持つatomic loadを、dead-code eliminationで削除しないようにします。 普通のpureな値の読み取りなら結果を捨てる処理を除去できても、atomic operationは同期の意味を持ちます。 返り値の使用有無だけでoperation全体を不要と判断すると、thread間の順序へ関わる処理を失います。 削除する最適化の条件を、atomic loadの意味へ合わせる修正です(atomic loadのPR #15045)。

pattern matchingには、immediate valueのrangeと冗長なpatternを組み合わせた場合のmiscompilationの修正があります。 sourceで書いたbranchの意味を、最適化後の分岐へ正しく移す問題です。 型が合う入力を拒否する診断の変更とは異なり、compileに成功したprogramが選ぶ処理へ影響します。 5.5.2の変更記録では、対象をrangeとredundant patternの特定の組み合わせへ限定しています(pattern compilationのPR #15185)。

Unix.map_fileには、data sizeを計算する際のinteger overflowを防ぐ修正があります。 要素数と要素の大きさから必要な領域を求める処理で、積が表現幅を超える入力を扱います。 領域の大きさを過小な整数として扱わない条件を加える変更です。 識別子はOSEC-2026-22で、通常の型として整数を受け取れることと、安全なsize計算が成立することを別に検査します。

Str.string_partial_matchでは、captureしたgroupが有効な範囲を持つかを検査します。 部分的な一致を返す経路で、groupの開始位置と終了位置を無条件に有効と扱わない修正です。 対象の識別子はOSEC-2026-21です。 文字列の一致判定と、一致に付随するcapture情報を後から読む条件を揃えます。

Marshal.from_channel、Marshal.from_string、Marshal.from_bytesには、複数のdemarshalingの修正が入りました。 対象はOSEC-2026-23です。 5.5.1にも全体のdata lengthの計算でoverflowを防ぐ修正があり、5.5.2では別の読み取り経路も修正されています。 文字列やbyte列からOCamlのobjectを構成する処理なので、sourceの型検査だけで外部dataの内部構造が正しいとは決まりません(5.5.1の長さ検査PR #15019)。

Windowsのcommand quotingには、OSEC-2026-24の修正があります。 program名や引数をcommand lineへ組み立てる処理では、quoteした文字列を次のparserが同じ引数として読む条件が必要です。 単に空白へ引用符を付ける操作と、Windows側の解釈へ整合するescapingは同じ処理ではありません。 5.5.2では、Windowsのquotingに関する複数の経路を対象にしています。

runtime eventsのring bufferには、messageをdecodeする処理を強化するOSEC-2026-25が入っています。 custom eventのcacheを整合した状態に保つ修正は、OSEC-2026-26です。 さらに、ring bufferの半分より大きいuser eventで無限loopへ入る問題も修正されました。 eventを出す側の実行中に、eventを読むlibraryだけ停止する経路も、更新の対象です(大きいeventのPR #15076)。

5.5.0の新構文を採用するsourceと、5.5.2のruntimeや最適化の修正を受け取る生成物は、移行する場所が異なります。 moduleやcallbackの新しい型はlibraryの公開signatureへ現れ、compilerの最低版を変えます。 size検査、threadの切り替え、分岐の生成といった修正は、同じsourceをcompileして動かす実装へ反映されます。 5.5系列を指定したbuildでも、正確なpatch versionが、生成したprogramと同梱runtimeの条件になります。