LunarMLをLuaの代替として使う際、Luaの機能を自然に使えると良さそうです。例えば、文字列フォーマット関数 string.format を呼び出す際には現状では引数の型キャストが必要ですが、フォーマット文字列にいい感じの型をつければキャストが不要になるのではないでしょうか。
ML系言語の仲間であるOCamlには、フォーマット文字列が期待される文脈で特別な型付けを行う機能があります。LunarMLでも似たようなことをするといいのではないか、というわけで検討します(検討するだけならタダなので)。
フォーマット文字列いろいろ
フォーマット文字列といえばC言語です。なので、まずはC言語のフォーマット文字列について再確認しておきます。Cのconversion specificationは次の形をしています:
%[flags][field width][precision][length modifier][conversion specifier]
flags, field width, precision, length modifierはオプショナルです。
flagsは以下の文字のいくつかの集まり(順序は問わない)です:
- マイナス 
-:左揃えになる - プラス 
+:符号つきの場合に符号がつく - スペース:符号つきの場合に先頭の文字が符号じゃなければスペースがつく。プラス 
+と共に指定された場合はスペースは無視される。 - ハッシュ 
#:代替形式 - ゼロ 
0:スペースの代わりにゼロでパディングする 
field widthは非負整数(十進)またはアスタリスク * です。変換後の文字列がfield widthより短かければ、スペース(デフォルト)あるいはゼロでパディングされます。アスタリスクを指定された場合は追加の引数として int を取ります。
precisionはピリオド . に続いて非負整数(十進)またはアスタリスク * です。浮動小数点数の場合は小数点以下の桁数を指定します。整数の場合は桁数の下限を指定します。アスタリスクを指定された場合は追加の引数として int を取ります。
length modifierは型の幅を指定するやつです。hh, h, l, ll, j, z, t, L のいずれかです。C23では w<N>, wf<N>, H, D, DD が増えます。
conversion specifierは一番重要なやつです。
d,i: 符号つき十進整数o,u,x,X: 符号なし整数(八進、十進、十六進)- C23では二進(
b,B)も増えます 
- C23では二進(
 f,F: 浮動小数点数、小数点以下の桁数は固定e,E: 浮動小数点数、指数形式g,G: 浮動小数点数、範囲によって切り替わるa,A: 浮動小数点数、十六進表記c: 文字コードs: 文字列p: ポインターn: これまでに出力された文字の個数を引数のint *に吐き出す。%:%という文字そのもの
Luaの string.format 関数では、Cのprintfライクな書式が使えます。printfの書式からいくつかのmodifierを抜いて、qというspecifierを足した形になっています。バージョンごとに微妙に差異があります:
Lua 5.4の場合:
F,n,*,h,L,lに非対応q: 文字列の場合はLuaの形式でエスケープ、クォートする。boolean, nil, numberの場合もLuaのリテラルとして読み戻せるような形式で書き出すsの引数が文字列でなかった場合はtostringを呼び出す。modifierがつく場合は\0は埋め込めないpは引数をポインターに変換する
Lua 5.3の場合:
F,*,h,L,l,n,pに非対応q: 文字列をLuaの形式でエスケープ、クォートするsの引数が文字列でなかった場合はtostringを呼び出す。modifierがつく場合は\0は埋め込めない
Lua 5.1の場合:
F,*,l,L,n,p,hに非対応q: 文字列をLuaの形式でエスケープ、クォートするsの引数の文字列には\0は埋め込めない- LuaJITの場合、整数としてFFIの 
int64_t,uint64_tも受け付けることがある(LuaJITのバージョンに依存する) 
LunarMLの出力先として考えられる他の言語もいくつか見ておきます。
PHPの printf ファミリー:PHP: sprintf – Manual
%[argnum$][flags][width][.precision]specifier
argnumは処理対象の引数の位置を指定するやつです。
flagsは以下を指定できます:
- マイナス 
-: 左揃え - プラス 
+:符号をつける - スペース:スペースでパディングする(デフォルト)
 - ゼロ 
0: ゼロでパディングする - クォート 
'(char): 任意の文字でパディングする 
widthやprecisionには * も指定できます。
specifierには以下を指定できます:
%:%そのものb,o,u,x,X: 符号なし整数(二進、八進、十進、十六進)c: ASCIIコードd: 符号付き十進整数e,E: 浮動小数点数の指数表記f,F: 浮動小数点数。fはロケール依存、Fはロケール非依存g,G: 浮動小数点数h,H: 浮動小数点数(g,Gの亜種)s: 文字列
Javaも見てみましょう。Formatter (Java SE 21 & JDK 21)
%[argument_index$][flags][width][.precision]conversion
flags:
-: 左揃え#: 代替形式+: 常に符号をつける- スペース:正の数に符号の代わりにスペースをつける
 0: ゼロでパディングする,: ロケール依存のgrouping separatorをつける(: 負の数を括弧で括る
conversion:
b,B: booleanh,H: ハッシュ値s,S: 文字列またはFormattablec,C: 文字d: 整数(十進表記)o: 整数(八進表記)x,X: 整数(十六進表記)e,E: 浮動小数点数(指数表記)f: 浮動小数点数g,G: 浮動小数点数a,A: 浮動小数点数(十六進表記)t,T: 日付と時刻%:%そのものn: 行区切り
言語によってフォーマット文字列に微妙な違いがあることがわかりました。
LunarMLではどうするか
基本的には、LunarMLがフォーマット文字列に対する型付けを提供するときはターゲット言語が提供するものに対応するようにしたいです。しかし、それでもいくつかの論点が考えられます:
- 整数系の指定子に 
intだけを許容するか、Int8.int/Int16.int/Int32.int/Int64.intなどのオーバーロードを許すか。ちなみにOCamlは整数型が少ないのでintだけを許容する感じのようです。 - Luaには組み込みの多倍長整数がないが、
IntInf.intを%dに許容するか。許容する場合はLunarML側でコード生成が必要になる。 
一つの指定子に複数の型を許す場合はアドホックなオーバーロード機構が必要になります。一応LunarMLには + などの組み込み演算子のオーバーロード機構がありますが、これらを拡張してフォーマットにも使う感じになるでしょうか。
OCamlはカリー化された形が基本なのでフォーマット関数もカリー化されているようです。Standard MLではそこまでカリー化を多用しません。LunarMLではどうするべきでしょうか。
いずれにせよ、型付けの都合からすると、フォーマット文字列は単独の引数として受け渡しすることになりそうです。残りの引数をタプルで渡す場合は
val format : 'args format_string -> 'args -> string (* 'args はタプル *)
となり、カリー化する場合は
val format : 'args_and_result format_string -> 'args_and_result (* 'args_and_result は a -> b -> c -> ... -> string という形の型 *)
となります。
正規表現
正規表現あるいはそれに類するもの(Luaのパターン)にも上手い型がつくと良いかもしれません。キャプチャーの個数に応じたタプルを返したり、キャプチャーがオプショナルだったら string option を返す、というようなものです。
これも言語によって微妙に仕様が違いそうなので、調査が必要です。
LunarMLの標準機能にするかどうか
LunarMLで特定のターゲットを前提にする場合はターゲット言語のフォーマット文字列を使えるのが自然です。一方、ターゲットに依存しないLunarMLの標準機能としてフォーマット文字列に類する機能を搭載するべきでしょうか?
一つの考え方は、ターゲット言語に依存しない中立なフォーマット文字列の形式を決めることです。
別の考え方は、あるターゲットのフォーマット文字列を別のターゲットでも使えるようにすることです。例えば LuaCompat.format という関数があったらそれはLuaでは string.format を使うようにして、Lua以外のターゲットでは自前でフォーマット処理を用意するのです。
あるいは、文字列を組み立てることに関してはフォーマット文字列ではなくstring interpolationを用意する方が良いかもしれません。string interpolationも色々考えることがありそうな機能です。
