実は,文字列は書き換えが可能。たとえば次のような文字列があったとして:
# let s = "life";; val s : string = "life" # s;; - : string = "life"
次のように書き換えができる。
# s.[2] <- 'k';; - : unit = () # s;; - : string = "like"
新しい値ができるのではなくて書き換わっている。
takatoh's blog – Learning programming languages.
実は,文字列は書き換えが可能。たとえば次のような文字列があったとして:
# let s = "life";; val s : string = "life" # s;; - : string = "life"
次のように書き換えができる。
# s.[2] <- 'k';; - : unit = () # s;; - : string = "like"
新しい値ができるのではなくて書き換わっている。
2つのデータを比べたとき,「値として等しいこと」を構造的等価性(structural equality)という。「値として」だけでなく,メモリ上の同じ位置を占めていることを物理的等価性(physical equality)という。
OCaml には構造的等価性を調べる演算子 = と物理的等価性を調べる演算子 == がある。
たとえば:
# "life" = "life";; - : bool = true # "life" == "life";; - : bool = false
2つの “life” は値としては等しい(構造的等価性)けど,メモリ上の同じ位置は占めていないので,== による比較(物理的等価性)では false になっている。
一方,いったん変数を束縛してやった場合には:
# s = "life";; - : bool = false # s = s;; - : bool = true # s == s;; - : bool = true
両方とも true になる。
ふーむ,このあたりは Ruby と同じだと思っておけばいいか。
レコードを宣言するときにフィールド名の前に mutable キーワードをつけることで,書き換え可能にすることができる。
# type teacher = {name : string; mutable office : string};; type teacher = { name : string; mutable office : string; }
これで office フィールドを書き換えることができるレコードができた。
具体的な値を作って:
# let t = {name = "Igarash"; office = "140"};; val t : teacher = {name = "Igarash"; office = "140"}
書き換えてみよう。書き換えは文字列の場合と似ていて,. (ドット)の後にフィールド名を書く。
# t.office <- "142";; - : unit = () # t;; - : teacher = {name = "Igarash"; office = "142"}
ちゃんと書き換わった。
書き換え可能なレコードの特殊な場合で,フィールド1つだけを持つ場合を伝統的に参照(reference)という。参照には特別な書き方がある。
まず,参照の生成には ref 関数。
# let p = ref 4;; val p : int ref = {contents = 4}
ref は初期値を引数にとって,参照を返す。ref は多相的な関数なので引数の型は何でもいい。
# let s = ref "foo";; val s : string ref = {contents = "foo"}
int ref とか string ref が参照の型。
参照の値を取り出すには前置演算子 ! を使う。
# !p;; - : int = 4 # !s;; - : string = "foo"
参照の書き換えは代入(assignment)と呼ぶことが多い。:= 演算子を使う。
# p := 7;; - : unit = () # p;; - : int ref = {contents = 7}
参照は型が決まっているので,違う型の値は代入できない。
# s := 77;; Characters 5-7: s := 77;; ^^ This expression has type int but is here used with type string
OCaml には書き換え可能なデータ構造がある。
Not_found とか Division_by_zero とかいう例外は,じつは exn型のコンストラクタ。例外コンストラクタと呼ぶ。
コンストラクタの型を見ると
# Division_by_zero;; - : exn = Division_by_zero
exn型であることがわかる。同様に raise の型も。
# raise;; - : exn -> 'a = <fun>
exn型には後からコンストラクタを追加することができる。これが新しい例外を宣言することに相当し,exception宣言を使う。
# exception Other_exception;; exception Other_exception # Other_exception;; - : exn = Other_exception
引数をとる例外の場合は of に続けて引数の型をかけばいい。
# exception Another_exception of string;; exception Another_exception of string # Another_exception "some error";; - : exn = Another_exception "some error"
整数リストの要素すべての積を返す関数 prod_list を定義しなさい。リスト要素の一つでも 0 が含まれている場合には,prod_list の適用結果は常に 0 になるので,例外処理を使用 して,0 を発見したら残りの計算を行わずに中断して 0 を返すように定義しなさい。
まずは 0 をかけるという例外を宣言しよう。
# exception Multiple_zero;; exception Multiple_zero
でもって prod_list の定義。
# let prod_list lis = let prod' x y = if y = 0 then raise Multiple_zero else x * y in try List.fold_left prod' 1 lis with Multiple_zero -> 0 ;; val prod_list : int list -> int = <fun>
これでいいはず。
# prod_list [1; 3; 7; 2; 9];; - : int = 378 # prod_list [5; 2; 0; 8; 4];; - : int = 0
OCaml には例外処理の仕組みがある。例外を発生させるには raise を使う。raise は例外の名前を引数にとる。
fact関数の引数が負の時に例外を発生する例:
# let rec fact n = if n < 0 then raise (Invalid_argument "fact: negative argument") else if n = 0 then 1 else n * fact (n - 1) ;; val fact : int -> int = <fun>
例外は関数の型とは関係がないので,気にせずに使える。
# fact (-2);; Exception: Invalid_argument "fact: negative argument".
発生した例外を捕捉して処理するのが try。
# try fact (-2) with Invalid_argument _ -> 0;; - : int = 0 # try fact 3 with Invalid_argument _ -> 0;; - : int = 6
match によるパターンマッチングに似ている。まず try と with に囲まれた部分を評価し,例外が発生したら with 以降の部分で例外名でパターンマッチして処理する。上の1番目の場合には例外が発生しているので値が0,2番目の場合には発生していないので値はそのまま6になっている。
Invalid_argument の後の _ は Invalid_argument の引数を示している。ここでは使わないのでワイルドカードにしているけど,きちんと書く場合には発生する例外と同じでないといけないらしい。
# try fact (-2) with Invalid_argumetn "fact: invalid argument" -> 0;; Characters 19-60: try fact (-2) with Invalid_argumetn "fact: invalid argument" -> 0;; ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Unbound constructor Invalid_argumetn
ということは,同じ名前の例外でも引数の違いによって処理を分けることができるのかな。
多相的関数が型情報をパラメータ化できるのと同じように,ヴァリアントの定義の一部をパラメータ化することができる。
# type 'a mylist = Nil | Cons of 'a * 'a mylist;; type 'a mylist = Nil | Cons of 'a * 'a mylist
‘a がパラメータ化された部分。このヴァリアントは Cons の引数にどんな型でも取ることができる。たとえば整数:
# let n1 = Cons (1, Nil);; val n1 : int mylist = Cons (1, Nil) # let n2 = Cons (2, n1);; val n2 : int mylist = Cons (2, Cons (1, Nil))
文字列でも:
# let s1 = Cons ("a", Nil);; val s1 : string mylist = Cons ("a", Nil) # let s2 = Cons ("b", s1);; val s2 : string mylist = Cons ("b", Cons ("a", Nil))