それでは、さっそく F# を使ってみましょう。シェル上で dotnet fsi を実行すると、対話モード (interactive mode) で F# を起動することができます。
$ dotnet fsi Microsoft (R) F# インタラクティブ バージョン F# 6.0 のための 12.0.0.0 Copyright (C) Microsoft Corporation. All rights reserved. ヘルプを表示するには次を入力してください: #help;; >
> は F# のプロンプトです。終了する場合は #quit;; と入力してください。Unix 系 OS の場合、Ctrl-D (Ctrl キーを押しながら d を押す) を入力しても終了します。プロンプトのあとに式を入力すると、F# は式を評価して結果を返します。
> 1 + 2 * 3;; val it: int = 7 > -3 * 4;; val it: int = -12
対話モードで式を入力する場合、最後にセミコロンを 2 つ ( ;; ) 入力してからリターンキーを押します。;; が入力終了のしるしになります。1 + 2 * 3 の結果を見ると、値が 7 でデータの種類が int であることがわかります。データの種類や種別のことを「データ型」、または単に「型」といいます。
F# の場合、数を表すデータ型の種類は C# と同じですが、名前が異なるものがあります。
F# の数はプリミティブな型で大きく分けると、整数 (int, int64 など) と実数 (float32, float など) の 2 種類があります。標準ライブラリ System.Numerics に用意されている構造体 BigInteger を使うと、任意の桁の整数 (多倍長整数) を扱うことができます。F# では bigint という型で扱うことができます。
実数は浮動小数点数 (floating point number) として表現されます。浮動小数点数には IEEE 754 という標準仕様があり、近代的なプログラミング言語のほとんどは、IEEE 754 に準拠した浮動小数点数をサポートしています。浮動小数点数はすべての小数を正確に表現することはできません。このため、実数は近似的 (不正確) な値になります。
なお、C# の場合、float は 32 bit 浮動小数点数を表す型ですが、F# では 64 bit 浮動小数点数を表すことに注意してください。32 bit 浮動小数点数は float32 になります。
数値のリテラルを記述するとき、数に接尾辞を付けることで型を明記することができます。
簡単な例を示しましょう。
> 123456789;; val it: int = 123456789 > 123456789L;; val it: int64 = 123456789L > 1.25;; val it: float = 1.25 > 1.25f;; val it: float32 = 1.25f
ここで、よく使われる算術演算子をまとめておきましょう。
OCaml とは異なり、F# は演算子のオーバーロードが可能なので、整数と実数の計算には同じ演算子を使うことができます。ただし、四則演算で整数と実数を混在させて計算することはできません。演算子の左右の値が同じデータ型になるよう注意してください。整数同士の四則演算でもデータ型が異なるとエラーになります。当然ですが、数式にはカッコ ( ) を使うことができます。
> 1.0 * 2.0;; val it: float = 2.0 > -3 * (5 - 2);; val it: int = -9 > 2 + 3L;; => エラー
F# にはデータを変換するための「変換演算子」が用意されています。名前は型と同じです。
> int 1.2345;; val it: int = 1 > float 12345;; val it: float = 12345.0
一つの文字を表す型を文字型 (char) といいます。F# (と C#) の場合、文字はユニコード (unicode) で表されます。日本語 (漢字やカナなど) を文字として扱うこともできます。文字は 'a' のように引用符 ' で囲んで表します。' を表す場合はエスケープシーケンスを使います。
> 'a';; val it: char = 'a' > '\'';; val it: char = '\'' > '\\';; val it: char = '\\' > 'あ';; val it: char = 'あ'
文字と整数の変換は、変換演算子で行うことができます。
> int 'a';; val it: int = 97 > char 97;; val it: char = 'a' > int 'あ';; val it: int = 12354 > char 12354;; val it: char = 'あ'
文字列 (string) は "foo" や "bar" のように二重引用符 ( " ) で囲みます。C言語と同様にエスケープシーケンスを使うことできます。たとえば、\n が改行で \t がタブになります。
> "foo";; val it: string = "foo" > "bar";; val it: string = "bar" > "foo" + "bar";; val it: string = "foobar" > "foo"[0];; val it: char = 'f'
文字列は演算子 + で連結することができます。それから、文字列[n] という形式で、文字列から n 番目の文字を取り出すことができます。
比較演算子は =, <>, <, >, <=, >= があります。値が等しいかチェックする述語が = で、等しくないかチェックする述語が <> です。簡単な例を示しましょう。
> 1 = 1;; val it: bool = true > 1 <> 1;; val it: bool = false > 1 <> 2;; val it: bool = true > 1 < 2;; val it: bool = true > 1 > 2;; val it: bool = false val it: bool = false > "foo" = "foo";; val it: bool = true > "foo" = "bar";; val it: bool = false
F# は真偽値を型 bool で表します。true が真で false が偽になります。比較演算子は整数や実数だけではなく、文字や文字列にも適用することができます。
F# には not, &&, || という論理演算子があります。
簡単な例を示します。
> not true;; val it: bool = false > not false;; val it: bool = true > 1 < 2 && 3 < 4;; val it: bool = true > 1 < 2 && 3 > 4;; val it: bool = false > 1 < 2 || 3 > 4;; val it: bool = true > 1 > 2 || 3 < 4;; val it: bool = true > 1 > 2 || 3 > 4;; val it: bool = false
条件分岐は if-then-else を使います。if E then F else G は最初に E を評価して、結果が真 (true) であれば式 F を評価し、偽 (false) であれば式 G を評価します。式 F または式 G の評価結果が if の返り値になります。式 F と G の返り値はどんな型でもかまいませんが、同じ型でなければいけません。型が違うとエラーになります。
簡単な例を示しましょう。
> if 1 < 2 then 3 * 4 else 5 * 6;; val it: int = 12 > if 1 > 2 then 3 * 4 else 5 * 6;; val it: int = 30
F# の場合、if-then-else の else は特別な場合を除き省略することができません。ご注意ください。
変数 (variable) は let 式で宣言します。
let 名前 = 式
Lisp などの関数型言語では、変数に値を割り当てることを「束縛 (binding)」といいます。純粋な関数型言語の場合、束縛された変数は値を書き換えることができません。手続き型言語は代入により変数の値を書き換えることができますが、純粋な関数型言語に代入操作はありません。ちなみに、Lisp は不純な関数型言語なので、変数の値を書き換えることができます。
なお、アポストロフィ ( ' ) から始まる名前は「型変数」になるため、変数名や関数名として用いることはできません。ご注意ください。また、F# は英大文字と英小文字を区別するので、たとえば foo と fOO は異なる名前になります。型変数はあとで詳しく説明します。
F# の場合、let で宣言された変数は、値を書き換えることはできません。これを immutable といいます。ただし、F# は純粋な関数型言語ではないので、変数名の前にキーワード mutable を付けると変数の値を書き換えることができます。このほかにも、値を書き換えることができるデータ構造、たとえば「配列 (array)」も用意されています。関数型言語は immutable が基本なので、mutable な機能はあとで説明することにします。
簡単な例を示しましょう。
> let a = 10;; val a: int = 10 > a;; val it: int = 10 > let b = 2.0;; val b: float = 2.0 > b;; val it: float = 2.0 > let c = "foo";; val c: string = "foo" > c;; val it: string = "foo"
対話モードの場合、変数名を入力するとその値が表示されます。なお、F# は同じ名前の変数を再定義することができます。
> a;; val it: int = 10 > let a = "foo";; val a: string = "foo" > a;; val it: string = "foo"
トップレベルで変数を再定義すると、元の変数は隠蔽されて値を参照することができなくなります。
F# は複数の型を組み合わせて新しい型を定義することができます。F# の場合、新しい型の定義方法はいくつかあるのですが、もっとも簡単で重要な方法がタプル (tuple) です。タプルは複数のデータや式をカンマ ( , ) で区切り、カッコ ( ) で囲んで表します。
簡単な例を示しましょう。
> let a = (1, 2);; val a: int * int = (1, 2) > let b = (10, 20.5);; val b: int * float = (10, 20.5) > let c = (1, 2.5, "foo");; val c: int * float * string = (1, 2.5, "foo") > let d = (1 + 2, 3 * 4);; val d: int * int = (3, 12)
変数 a のタプル (1, 2) は整数を 2 つ持っていて、型は int * int になります。このような型を「積型」といいます。積型は複数の型をアスタリスク ( * ) でつなげて表します。変数 b のタプル (10, 20.5) は整数と実数なので int * float になります。変数 c のタプル (1, 2.5, "foo") は int * float * string になります。また、最後の例のようにカッコの中に式を書くと、それを評価した値がタプルの要素になります。
タプルは入れ子にしてもかまいません。次の例を見てください。
> let a = ((1, 2), 3);; val a: (int * int) * int = ((1, 2), 3) > let b = (1, (2, 3));; val b: int * (int * int) = (1, (2, 3))
変数 a のタプルは、第 1 要素が int * int のタプルで、第 2 要素が int です。これを (int * int) * int と表します。変数 b のタプルは、第 1 要素が int で第 2 要素が int * int のタプルになります。これを int * (int * int) と表します。どちらのタプルも 3 つの整数が含まれていますが、型は異なることに注意してください。
タプルから要素を取り出すには、「パターンマッチング (pattern matching)」という機能を使います。次の例を見てください。
> let (a, b) = (1, 2);; val b: int = 2 val a: int = 1 > let (a, b) = ((1, 2), 3);; val b: int = 3 val a: int * int = (1, 2) > let ((c, d), e) = ((1, 2), 3);; val e: int = 3 val d: int = 2 val c: int = 1
let 式の右辺 (a, b) がパターンを表します。要素が 2 つ並んでいるので、2 要素のタプルを表すパターンになります。パターン (a, b) と左辺の (1, 2) を照合して、変数部分に対応する要素を取り出します。そして、変数をその値に束縛します。次の例のように、(a, b) と ((1, 2), 3) を照合すると、a は (1, 2) になり、b は 3 になります。
パターンは入れ子にしてもかまいません。((c, d), e) と ((1, 2), 3) を照合すると、c = 1, d = 2, e = 3 となります。このように、パターンを使ってタプルの要素を取り出すことができます。ただし、型が違うと照合に失敗してエラーになるので注意してください。
F# は関数も let で定義します。let のあとに名前と引数を書き、= のあとに引数を含む式を書きます。
let 名前 引数 = 式
たとえば、引数を 2 倍する関数 times2 を定義すると次のようになります。
> let times2 x = x * 2;; val times2: x: int -> int > times2;; val it: (int -> int) = <fun:it@48> > times2 4;; val it: int = 8
関数型言語の場合、関数もデータ型の一つです。let で指定した名前が times2 であれば、変数 times2 の値は関数型のデータになります。<fun ...> は値が関数であることを表し、型は "引数の型 -> 返り値の型" で表します。この型を見ると、関数 times2 は引数に int をひとつ取り、int を返すことがわかります。
ここで、引数や返り値の型を指定しなくても、F# が型を決めていることに注意してください。この機能を「型推論」といいます。times2 は引数と整数 (int) 2 の乗算を行っているので、引数は int で返り値も int になるはずです。このように F# が型を推論してくれるので、私達が型を指定しなくてもプログラムすることができます。
複数の引数を持つ関数を定義する場合はタプルを使うと簡単です。次の例を見てください。
> let f (x, y) = 2 * x + 3 * y;; val f: x: int * y: int -> int > f;; val it: (int * int -> int) = <fun:it@53-1> > f (1, 2);; val it: int = 8
関数 f は 2 つの引数 x, y を受け取ります。ここで関数 f の型 int * int -> int を見てください。引数の型が int * int の積型になっていますね。実をいうと、F# の関数は引数を一つしか受け取ることができません。複数の引数はタプルにして関数に渡します。つまり、関数呼び出し f (1, 2) は、タプル (1, 2) に関数 f を適用するという意味なのです。
タプルを使えば複数の値を返す関数も簡単に作ることができます。次の例を見てください。
> let foo (x, y) = - if x = y then (0, 0) - else if x < y then (-1, y - x) - else (1, x - y);; val foo: x: int * y: int -> int * int > foo (10, 20);; val it: int * int = (-1, 10) > foo (20, 10);; val it: int * int = (1, 10) > foo (10, 10);; val it: int * int = (0, 0)
関数 foo は引数 x と y の差分の絶対値を計算し、符号とその値を返します。if-then-else は else if でつなぐことができます。F# の場合、else if のかわりに elif を使うことができます。x = y ならば (0, 0) を返します。x < y ならば (-1, y - x) を返し、x > y ならば (1, x - y) を返します。このように、タプルを使って複数の値を返すことができます。
なお、引数や返り値の型は明示的に指定することができます。
let 名前 (引数 : 型) : 型 = 式
引数名の後ろにコロン ( : ) を付けて、その後ろに型を指定します。この場合、引数と型指定はカッコで囲んでください。返り値の型は、引数の後ろにコロンを付けて、その後ろに型を指定します。
簡単な例を示しましょう。
> let add(x, y) : int64 = x + y;; val add: x: int64 * y: int64 -> int64 > add (123L, 456L);; val it: int64 = 579L > let sub (x: float, y) = x - y;; val sub: x: float * y: float -> float > sub (1.234, 5.678);; val it: float = -4.444
引数と返り値をすべて指定しなくても、一部だけ指定すれば F# が型推論してくれます。
関数の引数は「局所変数 (local variable)」として扱われます。局所変数は「有効範囲 (scope : スコープ)」が決まっています。引数の有効範囲は、関数が定義されている式の中だけです。次の例を見てください。
> let x = 10;; val x: int = 10 > let y = 20;; val y: int = 20 > let bar y = x + y;; val bar: y: int -> int > bar 100;; val it: int = 110
局所変数として定義されていない変数は「大域変数 (global variable)」になります。大域変数はどこからでも値を参照することができます。対話モードで変数を定義すると、それらの変数は大域変数になります。最初に定義した変数 x と y は大域変数です。
関数 bar の引数は y で、式は x + y です。関数を呼び出す場合、引数用に新しいメモリを割り当て、そこに与えられた値で引数を束縛します。大域変数 y と引数 y は同じ名前ですが、異なる変数になるのです。そして、局所変数が定義されていれば、その値が参照されます。
局所変数が定義されていない場合、大域変数の値が参照されます。したがって、式の中の y は引数 y を参照し、bar の引数に x がないので、式の中の x は大域変数 x を参照します。よって、bar 100 は 10 + 100 = 110 になります。これを図に示すと次のようになります。
関数 bar を実行するとき、関数 bar の枠が作成されると考えてください。このとき、引数用に新しいメモリが割り当てられ、新しい局所変数 y が作成されるわけです。関数の実行が終了すると枠が壊されて、作成された局所変数も廃棄されます。関数 bar の場合、引数 y が廃棄されるので、対話モードでは大域変数 y の値を参照することができます。このように、関数の引数は関数定義されている式の中だけ有効なのです。
ところで、関数の中で引数以外の局所変数を定義できると便利です。F# の場合、let 式で局所変数を定義することができます。
let 変数 = 式1 in 式2
この let 式は、最初に 式1 を評価します。そして、変数をその結果に束縛して、式2 を評価します。その評価結果が let 式の返り値になります。なお、式1 や式2 が let 式でもかまいません。また、let 式を使って局所的な関数を定義することもできます。変数の有効範囲は let 式の中だけ、つまり式 2 の中だけになります。
たとえば、2 点間の距離を求める関数 distance を作ってみましょう。次のリストを見てください。
リスト : 2 点間の距離を求める (1) let distance ((x1, y1), (x2, y2)) : float = let dx = x1 - x2 in let dy = y1 - y2 in sqrt (dx * dx + dy * dy)
点の座標はタプル (x, y) で表します。引数として 2 つのタプル (x1, y1), (x2, y2) を受け取ります。x 座標の差分を局所変数 dx に、y 座標の差分を局所 dy に求めます。あとは、√(dx *. dx +. dy *. dy) を計算するだけです。
簡単な実行例を示しましょう。
> let distance ((x1, y1), (x2, y2)) : float = - let dx = x1 - x2 in - let dy = y1 - y2 in - sqrt (dx * dx + dy * dy);; val distance: (float * float) * (float * float) -> float > let p1 = (0.0, 0.0);; val p1: float * float = (0.0, 0.0) > let p2 = (10.0, 10.0);; val p2: float * float = (10.0, 10.0) > distance (p1, p2);; val it: float = 14.14213562
座標を表すタプルを変数に定義して、それを distance に渡します。すると、パターンマッチングにより、タプルの要素が取り出されて変数 x1, y1, x2, y2 にセットされます。
F# は OCaml と同様の構文と、Python や Haskell のレイアウトのようにインデントを利用した構文の二種類があります。前者を「冗語構文」、後者を「軽量構文」といいます。F# はデフォルトで軽量構文を認識します。軽量構文を使うと、入れ子になった let 式の in を省略することができます。
リスト : 軽量構文 (let の入れ子) let f x = let a = ... let b = ... let c = ... ...式...
let と最後の式を同じ位置に揃えます。インデントが変わると、そこで let の入れ子が終了したと判断されます。軽量構文を使って関数 distance を書き直すと次のようになります。
リスト : 2 点間の距離を求める (2) let distance ((x1, y1), (x2, y2)) : float = let dx = x1 - x2 let dy = y1 - y2 sqrt (dx * dx + dy * dy)
F# のコメントは // から行末までの「行コメント」と、(* から *) までの「ブロックコメント」があります。ブロックコメントは入れ子にすることもできます。行コメントの一種ですが、/// から行末までは XML ドキュメント用のコメントになります。コンパイラがコメントを抽出して、ドキュメントとして出力することができます。
次に示す関数または定数を定義してください。
> let square x : float = x * x;; val square: x: float -> float > square 2.0;; val it: float = 4.0 > square 2.5;; val it: float = 6.25 > let pi = 3.14159265359;; val pi: float = 3.141592654 > let circle_area r = pi * square r;; val circle_area: r: float -> float > circle_area 10.0;; val it: float = 314.1592654 > let medium (a, b) = (a + b) / 2.0;; val medium: a: float * b: float -> float > medium (2.0, 4.0);; val it: float = 3.0 > medium (2.0, 3.0);; val it: float = 2.5 > let square_medium (a, b) = medium (square a, square b);; val square_medium: a: float * b: float -> float > square_medium(2.0, 3.0);; val it: float = 6.5 > square_medium(1.5, 2.5);; val it: float = 4.25