OCamlのlambda IRをいじる(代数的データ型)
代数型データ型とそれに対するパターンマッチのlambda IRへのコンパイルを見ていく
タグのみ直和型
まずは簡単なタグのみ直和型を見てみる:
type t = A | B | C let () = let x = C in match x with | A -> print_int 1 | B -> print_int 2 | C -> print_int 3
type t = A | B | C
と直和型でタグ以外の型を何も持たせていない。列挙型といってもいいかも(言語によっては列挙型の意味も違うようだが・・・)
-dlambda
でコンパイル:
(seq (let (*match*/89 = (let (x/84 = 2a) (switch* x/84 case int 0: (apply (field 43 (global Stdlib!)) 1) case int 1: (apply (field 43 (global Stdlib!)) 2) case int 2: (apply (field 43 (global Stdlib!)) 3)))) 0a) 0a)
レコードの時と同じく、type t = ...
の部分は完全にlambda IRでは消えている。
let x = C in ...
が(let (x/84 = 2a) ...)
になっているので、タグのみ直和型の場合は0a, 1a, 2a
のような値にコンパイルされるようだ。
パターンマッチは(switch* ... case int 0: ... case int 1: ... case int 2: ...)
のような形になっている。
直和型&直積型
タグだけではなく、値も持った代数的データ型を定義・使ってみる:
type t = | A | B of int | C of string * bool let () = let x = A in let y = B 2 in let z = C("hi",true) in let f = function | A -> print_int 1 | B n -> print_int n | C(s,b) -> print_endline @@ if b then s else "hello" in f x; f y; f z;
-dlambda
でコンパイル:
(seq (let (*match*/95 = (let (x/84 = 0a y/85 = [0: 2] z/86 = [1: "hi" 1a] f/87 = (function param/92 (switch* param/92 case int 0: (apply (field 43 (global Stdlib!)) 1) case tag 0: (apply (field 43 (global Stdlib!)) (field 0 param/92)) case tag 1: (apply (field 45 (global Stdlib!)) (if (field 1 param/92) (field 0 param/92) "hello"))))) (seq (apply f/87 x/84) (apply f/87 y/85) (apply f/87 z/86)))) 0a) 0a)
タグのみのケースは先ほどと同じように0a
に、そして値を持つタグは[0: ...]
のような(タプルやリスト、レコードと同じ)ブロックにコンパイルされている。
値を持つタグが複数ある場合 [0: ...]
、[1: ...]
、[2: ...]
とブロックの最初の部分であるn:
で差別化している。(タプルのコンパイル結果を見たときからの「[0: ...]
の0:
部分はなんだ?」という疑問がここにきてようやく解ける)
パターンマッチは
- タグのみのケースは
case int 0:
- 値つきのケースは
case tag 0:
のようにコンパイルされており、case tag n:
が[n: ...]
のブロックと対応するようになっている。
直積型とタグ付きタプル
直積型とタグ付きタプルは、定義の仕方も使い勝手も非常に似ている:
type t = A of int * string | B of (int * string) let () = let x = A(1,"hi") in let y = B(2,"ho") in let f = function | A(n,s) -> print_int n; print_endline s | B(n,s) -> print_int n; print_endline s in f x; f y
A of int * string
とB of (int * string)
という違いだけで、あとはコンストラクタとしての使い方もパターンマッチもまったく同じ記法になっている。
-dlambda
でコンパイルしてみると:
(seq (let (*match*/95 = (let (x/83 = [0: 1 "hi"] y/84 = [1: [0: 2 "ho"]] f/85 = (function param/91 (switch* param/91 case tag 0: (seq (apply (field 43 (global Stdlib!)) (field 0 param/91)) (apply (field 45 (global Stdlib!)) (field 1 param/91))) case tag 1: (let (*match*/93 =a (field 0 param/91)) (seq (apply (field 43 (global Stdlib!)) (field 0 *match*/93)) (apply (field 45 (global Stdlib!)) (field 1 *match*/93))))))) (seq (apply f/85 x/83) (apply f/85 y/84)))) 0a) 0a)
実装は違っていることがはっきりわかる。
A of int * string
は二つのフィールドのあるブロック、B of (int * string)
はそれ自体がブロックである一つのフィールドを持つブロックになっており、パターンマッチに関してもデータの実装の違いが反映されている。
次回
次はミュータブルなref
がどのようにコンパイルされるのか見ていく。