「純粋関数型データ構造」を読み進めるための SML/NJ Tips
「純粋関数型データ構造」を読んでる. 中身は SML/NJ で書かれており,演習とかやってるとイロイロ調べるんで,そのまとめを.
正直,何番煎じだって感じですけど.
読み進めながら,少しずつ増やしていくつもりです.
(まだ,3章までしか読んでない)
環境
僕は,前回に自分が作った Docker イメージを使ってる.
その中身は SML/NJ のバージョン 110.81. Docker の OS は Debian だと思う.
Tips
対話型処理系
(Docker イメージは Jupyter なんでホントは関係ないんだけど)
sml
で対話型処理系が起動し,Ctrl-D
で終了する.
最後に ;
が無いと処理され始めないので注意.
サンプルコードについて
基本的なことはめんどくさいのでサンプルコードで解説
signature STACK =sig
type 'a Stack
val empty : 'a Stack
val isEmpty : 'a Stack -> bool
val cons : 'a * 'a Stack -> 'a Stack
val head : 'a Stack -> 'a (* スタックが空なら Empty 例外を投げる *)
val tail : 'a Stack -> 'a Stack (* スタックが空なら Empty 例外を投げる *)
end;
List : STACK =
structure struct
type 'a Stack = 'a list
val empty = []
fun isEmpty s = null s
fun cons (x, s) = x :: s
fun head s = hd s
fun tail s = tl s
end;
structure CustomStack : STACK =struct
Stack = NIL | CONS of 'a * 'a Stack
datatype 'a val empty = NIL
fun isEmpty NIL = true
false
| isEmpty _ = fun cons (x, s) = CONS (x, s)
fun head NIL = raise Empty
| head (CONS (x, s)) = xfun tail NIL = raise Empty
| tail (CONS (x, s)) = send;
signature
やstructure
ってのはモジュールsignature
はモジュールの型(インターフェース)みたいなものでstructure
はモジュールの値(実装)みたいなものsignature
で宣言されてる型や関数や値しか 外からは呼び出せない- ちなみに
signature
を省いてstructure
だけ書くこともできる
abc : 'a
は値(変数・関数)が'a
型であることを意味するtype
は型エイリアスdatatype
は代数データ型を定義するCONS of ...
でCONS
が...
型を引数に取るコンストラクタになる
'a * 'b
は'a
型と'b
型の直積型(組型)になる- e.g.
(1, "abc") : int * string
- e.g.
->
は関数型で'a -> 'b
は'a
型を貰って'b
型を返すことを意味するval
は変数定義fun
は関数定義|
でパターンマッチをしている
'a
は型変数- ML系はシングルクォート
'
を付けると型変数になる
- ML系はシングルクォート
type 'a Stack
はStack
型が型変数を1つ貰うこと意味する(多相型)- Haskell と違って前に型変数が付く
::
はリストの cons である- e.g.
[1] = 1 :: []
- e.g.
raise
は例外を投げているnull
,hd
,tl
は組み込み(正確には基本モジュール)のリスト型'a list
の関数- ref. The List structure - Standard ML
raise Empty
のEmpty
もリストモジュールで定義されている
(* ... *)
はコメントアウト
ちなみに,これらのモジュールは
List.empty
- val s1 = List.cons (1, List.empty)
- val s2 = CustomStack.cons (2, CustomStack.empty) -
のように使う. 前述したとおり,NIL
や CONS
を外から利用することはできない.
また,List.
や CustomStack.
を省きたいときは
open List -
とすればできるが,関数や変数のスコープが衝突することになるので注意.
データ構造を全部表示
例えば,ADS で定義した木構造なんかを出力すると
6, IntTree.insert (3, IntTree.insert (1, IntTree.insert (2, IntTree.insert (5, IntTree.empty)))));
- IntTree.insert (val it = T (T (T #,2,T #),5,T (E,6,E)) : IntTree.Set
って感じで,ある以上の深さは #
で省略されてしまう.
これを全部出力するためには
100; - Control.Print.printDepth :=
とすれば良い.
ref.
スコープを制限
適当に open
すると衝突するので
localopen CustomStack
in
fun append xs ys = if isEmpty xs then ys else cons (head xs, append (tail xs) ys)
end;
とすると,in ... end
だけでスコープを制限できる.
ファンクター
ファンクターはモジュール(structure
)を生成する関数のようなモノ. Haskell の型クラスのように,多相的な関数(など)に特定の制約,比較関数が定義されているとか,を持たせるために利用する.
(* 図 2.9 *)
signature SET =sig
type Elem
type Set
val empty : Set
val insert : Elem * Set -> Set
val member : Elem * Set -> bool
end;
signature ORDERED =sig
type T
val eq : T * T -> bool
val lt : T * T -> bool
val leq : T * T -> bool
end;
functor UnbalancedSet (Element : ORDERED) : SET =
struct
type Elem = Element.T
of Tree * Elem * Tree
datatype Tree = E | T type Set = Tree
val empty = E
fun member (x, E) = ...
fun insert (x, E) = ...
end;
structure IntElement : ORDERED =struct
type T = int
fun eq (x, y) = x = y
fun lt (x, y) = x < y
fun leq (x, y) = x <= y
end;
structure IntHeap = LeftistHeap (IntElement);val h1 = IntHeap.insert (1, IntHeap.empty);
as パターン
例えば,演習 2.1 の suffixes
を
fun suffixes [] = [[]]
| suffixes (x :: xs) = (x :: xs) :: suffixes xs;
としちゃうと,一度リストを展開して再度 cons するので,いわゆるポインタが変わっちゃう. 展開する前と後を同時に利用する方法が as パターン.
fun suffixes [] = [[]]
as (x :: xs)) = l :: suffixes xs; | suffixes (l
と書けばよい.
例外処理
例外の定義は exception
宣言で行う.
exception ExceptionName
signature
や structure
の中でも定義できる.
例外を発生させる場合は raise
キーワードを使い,補足する場合は handle
キーワードを使う.
fun hoge x = raise HogeException handle HogeException => x;
発生する可能性のある式の後ろに handle
を記述することで,その式の評価中に発生した例外を補足し,以降の処理を handle
より後で記述できる. handle
でパターンマッチすることもできる.
ref.
リスト処理
畳み込み関数 foldl
(Ruby でいう reduce
とか inject
とか)や,map
関数などの,よくある高階関数は,もちろん SML にもある. 以下で定義されており,とくにインポートする必要は無い.
ref.
ラムダ式(無名関数)
最近であれば,大抵のモダンな言語にあるラムダ式,SMLの場合は
fn x => x * x
と書く.
2引数関数の場合は
fn (x, y) => x * y fn x => fn y => x * y
のどちらか.
ref.
どう試すか
だいたいこうしてる. これは 図 3.2 の LeftistHeap
ファンクターを試している.
structure IntHeap = LeftistHeap (IntElement);val h1 = foldl IntHeap.insert IntHeap.empty [1,2,3,5,7];
val h2 = foldl IntHeap.insert IntHeap.empty [0,4];
IntHeap.findMin (h1);
IntHeap.findMin (IntHeap.deleteMin (h1)); IntHeap.findMin (IntHeap.merge(h1, h2));
おしまい
あと,証明とかで数式の性質なんかを調べるときに WolframAlpha が便利.