trifle

技術メモ

OCaml と TypeScript の比較に見るユーザ定義型

大学の授業の一つで OCaml という言語を今書いていて, ユーザ定義型について整理して学ぶ機会があったので, TypeScript と絡めてメモしてみます.


レコード型

レコードは組の各要素に名前が付いたもの.
OCaml での定義は type 型名 = {フィールド名: 型; ...}

OCaml だと, 例えば

type complex = { r: float; i: float };;

let add { r = a; i = b } { r = c; i = d } =
  { r = a +. c; i = b +. d };;
(* val add : complex -> complex -> complex = <fun> *)

add { r = 3.; i = 4. } { r = 5.; i = 6. };;
(* complex = {r = 8.; i = 10.} *)

こんな感じで, 複素数の足し算みたいなことができますね.

TypeScript だとオブジェクトに型名を付けるというのが一番近い形になると思います.

type complex = { r: number, i: number };

const add = (p: complex, q: complex): complex => {
  return { r: p.r + q.r, i: p.i + q.i };
}

console.log(add({r: 3, i: 4}, {r: 5, i: 6}));
// { r: 8, i: 10 }

TypeScript 独自の interface でも同じことができると思います.


ヴァリアント型

何種類かの値のうち一つをとる値であり, 型安全な操作ができます.
OCaml での定義は type 型名 = タグ1 of 型1 | タグ2 of 型2 ...

例えば

type value = VInt of int | VBool of bool;;

let string_of_value x =
  match x with
  | VInt i  -> string_of_int (2 * i)
  | VBool b -> string_of_bool (not b);;
(* val string_of_value : value -> string = <fun> *)

string_of_value (VInt 2);; (* string = "4" *)
string_of_value (VBool false);; (* string = "true" *)

こんな感じで, パターンマッチが使えます.

TypeScript の場合, union types が多分これに近いと思います.
パターンマッチの構文みたいなものはないです. typeof 演算子とかを使うと自動的に型を絞る判定をインラインではしてくれますが, パターンマッチと異なり, 全パターンを検査できているかをチェックするわけではないので, うーんという感じはします.

type value = number | boolean;

const string_of_value = (x: value): string => {
  if (typeof x === "number") {
    // この時点で x が number 型だと自動で判定される
    return String(2 * x);
  } else {
    // この時点で x が boolean 型だと自動で判定される
    return String(!x);
  }
}

console.log(string_of_value(2));  // 4
console.log(string_of_value(false));  // true


多相型

型によらず本質的に同じ動作をさせたい場合は, Polymorphism をやっていきましょう.
例えば OCaml で二分木を用意すると...

type 'a tree =
  | Leaf
  | Node of 'a * 'a tree * 'a tree;;

Node (3, Leaf, (Node (5, Leaf, Leaf)));;
(* int tree = Node (3, Leaf, Node (5, Leaf, Leaf)) *)
Node ("hoge", (Node ("fuga", Leaf, Leaf)), Leaf);;
(* string tree = Node ("hoge", Node ("fuga", Leaf, Leaf), Leaf) *)

色んな型の木が用意できていますね.


TypeScript の場合, 多相性を実現するのはジェネリクスです. ただ, OCaml の例のように, 型の中に直接多相性を持ち込むことはできないようです.
(ドキュメントを読むと, TypeScript でジェネリクスが使われるのはクラスやインターフェース, 関数の定義の中のようです).
例えば TypeScript のクラスでやると二分木の初期状態はこんな感じになるんじゃないかと...

class Tree<T> {
  value: T | undefined;
  left: Tree<T> | undefined;
  right: Tree<T> | undefined;
}

const btree = new Tree<string>(); // 最初の何もない木
console.log(btree.value); // undefined
btree.value = "test";
console.log(btree.value); // test

ここからうまく挿入や削除の関数もジェネリクスを使って書けそうです. 今度やってみようかなと思います. (5/1追記: やりました https://qiita.com/7ma7X/items/3f1e40ef142614d40210




初歩的な内容にとどまりましたが, ユーザ定義型について理解が深まったのでよかったです.