SMLのDerived Forms

このエントリはML Advent Calendar 2020の21日目の記事です。 前はelpinalさんで「長さn以上のリストをファンクタで」、後はnymphuimさんで「OCaml の小ネタを書く」です。

κeenです。小ネタなんですが、SMLのDerived Formsについて書きます。

Derived Formsとは

The Definition of Standard ML (revised)では基本文法の他にAppendix AでDerived Forms(派生形式)というのが定義されています。平たくいうと糖衣構文ですね。

普段SMLのコードを書く上ではあまり意識する必要はないのですが、SMLのコンパイラを作るときにはよく確認することになります。そしてDerived Formsを見ると「へー、XXって実はYYの構文糖衣だったんだ」と気付くことがあります。この記事ではそういう「へー」を紹介しようかなと思います。

意外なDerived Forms

タプル

これは有名ですかね。タプルはラベルが数値なレコードです。(){} です。

- () = {};;
val it = true : bool
- {1 = "one", 2 = "two"};;
val it = ("one","two") : string * string

ただしラベルは1はじまりの2以上なので {1 = "one"} に対応するタプルはありません。

- {1 = "one"};
val it = {1="one"} : {1:string}

余談ですが SML# で () = {} を試したらエラーになりました。

SML# 3.6.0 (2020-05-29 09:58:49 JST) for x86_64-pc-linux-gnu with LLVM 10.0.1
# () = {};;
(interactive):1.0-1.6 Error:
  (type inference 026) operator and operand don't agree
  operator domain: unit * unit
          operand: unit * {}
# {};;
val it = {} : {}
# ();;
val it = () : unit

仕様違反なのでバグですかね。

ラベル

タプルの要素は #n で、レコードの要素は #label で取り出せますね。これもDerived Formです。

fn {label = tmp, ...} => tmp へと展開されます。

andalsoorelse を最後まで展開すると…

exp1 andalso exp2 はDerived Formです(orelse も同じ、以後同様)。 Derived Formsを展開した結果さらにDerived Formsが現われたらまた展開することになっています。最後にはコア言語に到達して展開が停止します。では exp1 andalso exp2 をコア言語まで展開したらどんな式になるか分かりますか?

  • exp1 andalso exp2if exp1 then exp2 else false へと展開されます。
  • if exp1 then exp2 else exp3case exp1 of true => exp2 | false => exp3 へと展開されます。
  • case exp of match(fn match)(exp) へと展開されます。

これらを総合すると

  1. exp1 andalso exp2
  2. if exp1 then exp2 else false
  3. case exp1 of true => exp2 | false => false
  4. (fn true => exp2 | false => false)(exp1)

と展開され、最後には fn とその適用になります。 そもそも fnfn pattern1 => expr1 | pattern2 => expr2 ... という構文なんですがご存知でしたか?

セミコロン

let ... in expr endexpr の部分ではセミコロンで区切って複数の式が書けます。

- let in print "hello"; print ", world\n" end;;
hello, world
val it = () : unit

このセミコロンもDerived Formです。 let ... in expr1; expr2; ...; exprn; exp endlet ... in (expr1; expr2; ...; exprn; exp) end へと展開されます。 そして (expr1; expr2; ...; exprn; exp) もDerived Formです。これはちょっとびっくりですが、case 式の連鎖へと展開されます。

case expr1 of (_) =>
case expr2 of (_) =>
...
case exprn of (_) => exp

この後 casefn へと展開されていきます。展開結果を見るのがちょっと怖いDerived Formですね。

ファンクタ

逆に「あ、そんな構文あったんだ」となるのがファンクタの引数。 ファンクタを適用するときにこんなコード書きますよね?

structure CharHashTable = HashTable(struct
  val eq = Char.eq
  val hash = Char.hash
end
)
(* 適当コード、多分コンパイルは通らない *)

実は structend はなくてもOKです

structure CharHashTable = HashTable(
  val eq = Char.eq
  val hash = Char.hash
)
(* 適当コード、多分コンパイルは通らない *)

structureの構文を書いたら structend が補われ、 Struct とstructureの名前を書いたらそのまま使われる仕組みになってます。パーサが大変そうですね。

it

SMLでREPLを使うとき、直前の式の評価結果に it を束縛してくれるので次の入力で直前の結果を it として参照できますよね?

- 1 + 2;;
val it = 3 : int
- it * 3;;
val it = 9 : int

実はこれも仕様で定められたDerived Formです。

書き換え規則は exp;<program>val it = exp;<program> です。 REPLで式を直接入力できる理由が分かりましたね。

仕様にあるのでREPLだけでなくファイルでも使えます。 流石に it を使う人はいないかと思いますが、トップレベルに式を直接書いてそのまま実行されるのを期待する人は多いんじゃないでしょうか。

以上、SMLのDerived Formsの紹介でした。

Written by κeen