関数と型推論
前回はデータ型や演算子などの基本作法を扱った。
今回はCamlの中心的な特徴となる型推論と関数を扱う。
型推論
前回のコードを見ればわかると思うが、ユーザーがCamlは型を明示せずとも自動で判別を行ってくれる。これが型推論である。
当然明示的に指定することも可能で、その場合は":"を使う(例2-1)。ただし、間違えば当然エラーをはくので注意。
ちなみに既に前回のコードでお気づきかもしれないが、インタプリタは問題となる(と判断した)箇所まで指摘してくれる親切仕様だ。
明示的な型指定(例2-1)
# let x:int = 10;;
val x : int = 10
# let y:string =10;;
Characters 14-16:
let y:string =10;;
^^
This expression has type int but is here used with type string
# let y:string = "10";;
val y : string = "10"
関数
関数定義もletによる束縛で表現する。
引数を用いる関数は let f x = func という書式で表記する(xを受け取りfuncを実行する関数f)。
宣言後の出力は val f: int -> int = fun となり、受け取る引数の型 -> 出力する値の型 というように表記される。ここでも型推論が行われているのがわかる。
ただ、型推論ができない場合、すなわちどんな型でもかまわない場合は多層型α(表記上は'a)と判別され、 'a -> 'aという出力がなされる。ただし、ここで'aはすべて同じ型であり、動作上変数が異なる型でなければいけない場合は 'a 'b 'cと別の文字で型が与えられる。(例2-2)
# let f x=3+x;;
val f : int -> int =
多層型変数による関数定義(例2-2)
# let id x=x;;
val id : 'a -> 'a =
# let id2 x y=y;;
val id2 : 'a -> 'b -> 'b =
なお、関数の評価結果は最後に評価された値であり、retunで指定するなどはできない。
関数の適用は、関数名の後に引数をつければ良い。(構文解析の都合で負の値は括弧でくくる必要があるので注意)
複数の引数とカリー化
また、引数は複数指定することも可能。
やり方としては2種類あり、一つは引数を組にして入力する方法、もう一つは引数を順に入力する方法である。
前者は let f(x,y) = func のように表記。
その出力は int * int -> int = fun のようになる。
後者は let f x y = func のように表記。
出力は int -> int -> int = fun
Camlでは後者のやり方が主流で、カリー化と呼ばれる。(例2-3)
前者と後者は関数の型が違うため、混同できないことに注意。
複数の引数を持つ関数の宣言(例2-3)
# let f (x,y) = x+y ;;
val f : int * int -> int =
# let f x y = x+y ;;
val f : int -> int -> int =
なお、ここまで何度も断りなく同じ変数や同じ関数名を使用してきているが、型が違えば同じ変数名、関数名が同時に利用できるため問題なく走っているにすぎないことに注意。(上の例だと、f(x,y)もf x y もともに存在している。)またさらに言えば、既に束縛された変数名を引数として利用しても問題は発生しない。(引数の適用範囲が関数内に限られるため)
(なぜこんな断りを入れるかと言えば、読み直したときに自分が忘れていそうだからである)
再帰関数
再帰関数はlet ではなく let rec を用いて束縛する。
let recによる束縛は、その定義内で既に関数を束縛するため、関数定義の中で自身を呼び出せるようになる。
対してletによる束縛は宣言終了まで束縛がなされないため、定義中に自身を呼び出すことはできない。(例2-4)
再帰関数の定義(例2-4)
# let rec f x = if x<=0 then 0 else x+f(x-1);;
val f : int -> int =
# f 3;;
- : int = 6
# let g x = if x<=0 then 0 else x+g(x-1);;
Characters 32-33:
let g x = if x<=0 then 0 else x+g(x-1);;
^
Unbound value g
ループと末尾再帰
OCamlでは、ループの代わりに再帰を用いるのが一般的。
その場合はリソースを有効活用するために末尾再帰を用いる。
(なお、公式サイトのドキュメントでは「再帰は非効率」の主張に対する反論と再帰を用いる根拠が示されているので興味のある人はそちらを参照のこと)
末尾再帰とは関数の最後に再帰呼び出しを行うことで、これを用いると自身の値と呼び出した値が一致する。そのために新たな関数を一つ宣言したりするのだが、とりあえずここではサンプルコードを参照して欲しい。(それでなんとか思い出せ、自分。)
通常の再帰プログラム
# let rec sum n=
if n<=0 then
0
else
n+sum(n-1)
;;
val sum : int -> int =
# sum 10;;
- : int = 55
末尾再帰を用いたプログラム
# let sum n =
let rec iter i s =
if i <= 0 then
s
else
iter(i-1)(s+i) in iter n 0
;;
val sum : int -> int =
# sum 10;;
- : int = 55
以下覚え書き
in iter n 0 でsum nの内部での実行として呼び出している
あとは(i-1)(s+i)の蓄積。(i,s)は(n,0)から(0,n)まで。
とりあえず1コマ90分でここまで完了。
あと2,3週もあれば講義に追いつきそう。
そして試験まで後3,4週だから・・・