OCaml(関数型言語):Dev Basics/Keyword
OCamlは記述性が高く、型安全なプログラミングを実現する関数型言語であり、静的型付けと強力な型推論、バリアント型などがその特徴として挙げられる。
OCamlは記述性が高く、型安全なプログラミングを実現する関数型言語だ。その特徴としては、静的型付けと強力な型推論、オブジェクト指向プログラミングのサポート、バリアント型などが挙げられる。
OCamlの特徴
OCamlはML(Meta Language)と呼ばれる関数型言語から派生した言語(方言)であり、フランスのINRIA研究所が開発した。OCamlの前身に当たるのがCamlと呼ばれる言語であり、これにオブジェクト指向プログラミング用の型システムが統合され、Objective Camlが誕生した。その後、さまざまな拡張が行われていく中で、2011年にその名称がOCamlに変更された。OCamlの歴史については「A History of OCaml」を参照されたい。
「What is OCaml?」ページではOCamlが提供する機能およびOCamlの特徴として以下が挙げられている。
- 強力な型システム
- 代数的データ型とパターンマッチ
- 自動メモリ管理(ガベージコレクション)
- 独立したアプリのコンパイル
- 洗練的なモジュールシステム
- オブジェクト指向プログラミングを可能にするレイヤー
- ネイティブコードコンパイラ
OCamlは強い静的型付け言語であり、コンパイル時に静的型チェックが行われ、型に関連するエラーが検出される(これは対話的なREPL環境でも同様であり、プロンプトに対して入力された式はコンパイルされ評価される)。その一方で、型推測機構があるため、実際のコードでは型宣言を記述する必要がない(ただし、型推測を支援するために、他の言語ではよく見られる暗黙的な型変換は許されていない。また、四則演算でも整数型と浮動小数点数型では別の演算子が使用される)。
OCamlの型システムは非常に強力であり、コンパイル時に多くのバグを捕捉できる。代数的データ型とは「複数の型を組み合わせて作られる型」のことであり、OCaml(に限らず多くの関数型言語)でよく言及される概念である。OCamlでは「バリアント型」を使用して、複数の型で示されるいずれかの値を取るデータを表現できる(後述)。パターンマッチは、何らかの複合的な値(例えばタプルやリスト)とパターンを比較して、処理を分岐させたり、そこから個別の値を取り出したりするのに使われる。
簡単なサンプル
以下では、OCamlの対話型コンパイラを使用して、OCamlの特徴を幾つか見ていく(使用したのはWindows版のOCaml)。
単純な計算
まずは簡単な例を見てみよう。
# 4 + 3;;
- : int = 7
# 4.0 +. 3.0;;
- : float = 7.
# 4 + 3.0;;
Error: This expression has type float but an expression was expected of type
int
# float 4 +. 3.0;;
- : float = 7.
OCamlのREPL環境(対話的コンパイラ)では、入力の終了を表す「;;」が必要になる。また、2つ目の例を見ると分かるように、浮動小数点数の演算では「+.」のようにピリオド付きの演算子を使用する。最後の2つの例では暗黙の型変換が許されず、明示的な型変換が必要になることが分かる。この場合は「float 4」で整数値を浮動小数点数値に変換しているが、これは「float_of_int 4」の別名であり、一般には「変換後のデータ型_of_変換前のデータ型」という名前の関数が用意されている。
次に変数に値を束縛し、それらを関数で使用してみる。
# let a = 10;;
val a : int = 10
# print_int a;;
10- : unit = ()
# print_endline ("value of a: " ^ string_of_int a);;
value of a: 10
- : unit = ()
変数に値を束縛するにはletキーワードを使用する。REPL環境の出力「val a : int = 10」を見ると分かる通り、ここでは型推測が機能して、この値がint型であるとされている。また、ここではprint_int関数とprint_endline関数を使用している。関数の呼び出しにはかっこは不要であり、一般には「function arg1 arg2 ……」のような形式で呼び出せる。その一方で、print_endline関数では引数部分である「"value of a: " ^ string_of_int a」をかっこで囲んでいるが、これは引数を明確にするためだ(「^」は文字列を連結する演算子でstring_of_int関数は整数値を文字列に変換する関数)。これらの関数の評価結果が「unit = ()」となっている。これはその関数が値を返さないことを意味する。
関数
次に関数を定義してみる。
# let hello name =
print_endline ("Hello " ^ name);;
val hello : string -> unit = <fun>
# hello "World";;
Hello World
- : unit = ()
OCamlでは関数も第一級のオブジェクトであり、上で見たようなletによる変数への値の束縛と同様にして定義できる。関数名の右にはパラメーターを記述する。OCamlコンパイラが静的解析を行った結果、hello関数は「文字列を引数として何も返さない」と推測できている。これを示すのが「val hello : string -> unit = <fun>」というREPL環境からの出力である。
今度はパラメーターを2つ持つ関数を定義する。
# let sum1 x y =
x + y;;
val sum1 : int -> int -> int = <fun>
# let sum2 x = fun y -> x + y;;
val sum2 : int -> int -> int = <fun>
sum1関数とsum2関数はどちらも渡された2つの値の和を求める関数だが、後者ではfunキーワードを使用して匿名関数を作成している。すなわち、sum2関数は「パラメーターを1つ受け取り、sum2関数が受け取ったパラメーターとの和を返す関数」を返す関数である。これを示しているのが「int -> int -> int = <fun>」である。そのため、以下のようなことが可能だ。
# sum2 1 2;;
- : int = 3
# let adder2 = sum2 2;;
val adder2 : int -> int = <fun>
# adder2 3;;
- : int = 5
最初の「sum2 1 2;;」は「(sum2 1) 2;;」と同値であり、「sum2 1」により「パラメーターに1を加算して返す関数」が返され、今度は引数に「2」を指定してその関数が呼び出されている。
sum1関数の型もsum2関数と同じ「int -> int -> int = <fun>」であることに注意。sum2関数の定義はsum1関数の定義を「カリー化」(複数のパラメーターを持つ関数を、単一パラメーターを持つ複数の関数の連なりに)したものである。そして、sum1関数についても「let adder3 = sum1 3;;」のようなことが可能だ。
関数の例の最後に再帰関数を見てみる。これは指定した数までの総和を求める関数だ。
# let rec sum_to n =
if n = 0 then 0
else n + sum_to (n-1);;
val sum_to : int -> int = <fun>
# sum_to 4;;
- : int = 10
再帰関数を定義するには「rec」キーワードを指定する。コードの内容自体は特に説明の必要はないだろう。もっとも、総和を求めるのであれば、以下のように計算する方がよいだろう。
# let sum_to2 n =
n * (n+1) / 2;;
val sum_to2 : int -> int = <fun>
あるいは、末尾再帰を行う以下のようなコードも書ける。
# let rec sum_to3 (n, count) =
if n = 0 then count
else sum_to3 (n - 1, count + n);;
val sum_to3 : int * int -> int = <fun>
ここでは、総和を求める上限の数とカウンター変数をタプルにまとめたものがパラメーターとなっていることに注意。
バリアント型
OCamlでは、さまざまな形態を取るデータ型を「バリアント型」を使用して定義できる。以下に例を示す(バリアント型に限らず、ユーザー定義の型はtypeキーワードを使用して定義する)。
# type signal =
|BLUE
|YELLOW
|RED;;
type signal = BLUE | YELLOW | RED
BLUE、YELLOW、REDは「コンストラクタ」と呼ばれる(実際には他の言語に見られるコンストラクタとは異なるが詳細は割愛)。signal型のデータを作成するには、このコンストラクタを記述してやる。コンストラクタは「コンストラクタ of 型名」のようにも記述できるがこれについては後述する。
# let state = BLUE;;
val state : signal = BLUE
これを使用する関数の例を以下に示す。
# let at_crossing = function
|BLUE -> print_endline "go"
|YELLOW -> print_endline "watch out!"
|RED -> print_endline "stop!";;
val at_crossing : signal -> unit = <fun>
# at_crossing state;;
go
- : unit = ()
at_crossing関数はパラメーターに受け取った値に対してパターンマッチを行い、その種類によって処理を切り替えている。上の例では青信号なので止まらず進んだということだ。このようにバリアント型はC言語などでの列挙型のようにも使える。
引数を持つコンストラクタは次のようになる。
# type foo =
|Int of int
|Float of float
|String of string;;
type foo = Int of int | Float of float | String of string
# let n = Int 100;;
val n : foo = Int 100
# let f = Float 1.0;;
val f : foo = Float 1.
# let s = String "string";;
val s : foo = String "string"
3つあるコンストラクタはそれぞれ整数、浮動小数点数、文字列を引数に取る。3つの変数n、f、sは実際には異なる型のデータを保持するにもかかわらず全てfoo型の変数となる。
上の例はシンプル過ぎて分かりにくいのだが、状況に応じて、異なる属性を持つ、あるいは異なる形態を取るデータを統一的に扱えるようになるのが、バリアント型の大きなメリットとなっている。詳細についてはオライリーから刊行されている「Real World OCaml」のオンライン版の第6章などが参考になるはずだ。
バリアント型は「直和型」と呼ばれるデータ型のカテゴリに分類される。これは、バリアント型のデータはそこで指定されているいずれかの値となる、つまり、バリアント型のデータ型の取り得る範囲が、バリアント型のコンストラクタとして記述されている型が取り得る範囲の「和」となるからだ(上のfoo型であれば、その型のデータが取り得る範囲は「int型で表現できる範囲+float型で表現できる範囲+文字列で表現できる範囲」となる)。
これに対して、タプルやレコードなどは「直積型」と呼ばれるカテゴリに分類される。これは、タプルやレコードはその要素あるいはフィールドなどから構成され、そのデータが取り得る範囲はその要素/レコードが取り得る値の全組み合わせ(積)となるからだ(2つのint型のフィールドで構成されるタプルが取り得る範囲は「int型で表現できる範囲×int型で表現できる範囲」となる)。
OCamlに限らず、関数型言語では「代数的データ型」という概念が一般的に使われており、上で述べた直積型と直和型(加えて列挙型)をまとめて代数的データ型と呼ぶ。OCamlではバリアント型を導入することで、直和型のデータ型と直積型のデータ型を統合的に扱えるようになっている。
OCamlは記述性が高く、型安全なプログラミングを実現する関数型言語だ。本稿では、関数を中心にその特徴を幾つか紹介してきた。ただし、オブジェクト指向プログラミング、OCamlが持つ命令型言語的な側面、型パラメーターを使用した多相性など、他にもOCamlを特徴付ける機構は数多く存在している。これらについては以下の参考資料を参照してほしい。
参考資料
- OCaml: OCaml公式サイト
- OCaml.jp: OCamlに関する情報を集めたコミュニティーサイト
- OCamlチュートリアル: 公式サイト内のチュートリアルページ(日本語)
- Real World OCaml: オライリーより刊行されている「Real World OCaml」のオンライン版(英語)
Copyright© Digital Advantage Corp. All Rights Reserved.