Table of Contents
関数型言語では、式(expression)がプログラムの基本要素となります。ここまでの章でも、さまざまな式を使ってきました。たとえば前の章の最後で紹介した、束縛式もひとつの式であり、3
や"Hello"
といったリテラルも、単純定数式(simple
constant expression)というひとつの立派な式です。
このように、式には多くの種類が存在しますが、この章ではそれらのうち制御フロー式(control flow expression)という種類のものを紹介します。誤解を恐れずにいうと、F#のプログラムはすべてが式であり、F#のプログラムを作ることは、複数の小さな式を組み合せて1つの巨大な式を作ることと等しいといえます。この章で紹介する制御フロー式は、複数の式を組み合わせるための糊のひとつです。
複合式(compound expression)はセミコロンで区切られた複数の式を左から順番に評価し、一番右の式の評価結果を式全体の値とします。一番右以外の式の評価結果は捨てられてしまいます。構文は以下のとおりです。複合式は、文法的にもれっきとしたひとつの式であることに注意してください。
expr1
;
expr2
> printf "Hello,\n"; printf "world\n";; // Helloを出力してからworldを出力する
Hello,
world
軽量構文ではセミコロンのかわりに改行を使うことができます。
> printf "Hello,\n"
printf "world\n";;
Hello,
world
軽量構文ではインデントは重要な意味を持ちます。F#インタプリタでは、標準で軽量構文が有効になっているため、インデントの位置には気をつける必要があります。ただし、F#インタプリタにおけるプロンプト(> )の2文字は、インデントの文字数として数えないように注意してください。
ここで注意しなければいけないのが、expr1
の評価結果の値は捨てられてしまうため、unit
型以外の値では警告が出る場合があります。もしexpr1
が返す値に興味がなく、捨ててしまいたい場合は標準ライブラリのignore
関数を使うことができます。
ignore
関数は非常に単純な関数で、あらゆる型の値を受け取りますが、受け取った値は無視してすぐにunit
を返します。つまり、単純に戻り値を破棄するだけの役割を持ちます。
> ignore(1 + 2);; // 1+2を計算するがその戻り値は破棄してunitを返す val it : unit = () > System.Windows.Forms.MessageBox.Show("Hello, world!");; // MessageBox.Showは戻り値としてDialogResultを返す val it : System.Windows.Forms.DialogResult = OK > ignore(System.Windows.Forms.MessageBox.Show("Hello, world!"));; // MessageBox.Showの戻り値を捨てる val it : unit = ()
条件式(conditional expression)は、もっとも基本的な条件分岐構文です。条件式の構文は以下のとおりです。
if boolean_cond1
then
expr1
[elif
boolean_cond2
then
expr2
]*
[else
expr3
]
boolean_cond1
、boolean_cond2
はbool
値の式であり、expr1
、expr2
、expr3
はすべて同じ型の式である必要があります。else
を省略した場合はexpr1
、expr2
はunit
型でなければなりません。具体的な例をみてみましょう。
> let x = 10;; // xに10を束縛 val x : int 10 > if x > 0 then 1 else -1;; // xが正なら1、それ以外は-1を返す条件式 val it : int = 1 > if x < 0 then 1 else -1;; // xが負なら1、それ以外は-1を返す条件式 val it : int = -1 > if x > 0 then 1 elif x < 0 then -1 else 0;; // xが正なら1、負なら-1、それ以外は0を返す条件式 val it : int = 1 > if x < 0 then 1 else -1.0;; // xが負なら1、それ以外は-1.0を返す条件式 if x < 0 then 1 else -1.0;; ---------------------^^^^^ stdin(61,22): error FS0001: This expression has type float but is here used with type int.
最後の入力のエラーは、expr1
とexpr3
の型が異なっているために発生しています。
match式(match
expression)は、基本的な考え方はC#のswitch
文を拡張したようなもので、F#における非常に強力な表現力をもつ構文です。構文は以下の形になります。
match test_expr
with
rules
この式は、text_expr
の評価結果をrules
に書かれたルール(通常は複数個ある)に対して、後述するパターンマッチ(pattern
match)というものを行っていきます。そしてマッチするルールが存在すれば、そのルールの右に書かれた式を評価します。
> let x = 5;; val x : int > match x with - | 3 -> printf "Three\n" - | 5 -> printf "Five\n" - | _ -> printf "Otherwise\n";; Five val it : unit = () > match x with 3 -> printf "Three\n" | _ -> printf "Otherwise\n";; Otherwise val it : unit = ()
このmatch
式では、まずx
が5だと評価されます。次にその値を、下の3つのルールに順番にパターンマッチしていきます。この場合は2番目のパターンにマッチし、printf
"Five\n"
が評価されて、画面にはFive
が出力されています。3つめのルールは、ワイルドカードパターン(wild
card
pattern)というもので、どんなものにもマッチします。ちょうどC#のswitch
文におけるdefault
ラベルのようなものです。
match
式が強力であるゆえんは、ひとえにパターンマッチの強力さによるものです。この例では、3
、5
、_
という値を比較するだけの単純なパターンしか使っていないので、その強力さはわかりませんが、実際には非常に多くのバリエーションがあります。それらについて次節で説明します。
以下の式は、すべてlet x
=
の右側に制御フロー式によって組み合わされた1つの式があり、この式を実行すると、x
にその式の最終的な値が入ります。それぞれx
にはどのような値が入るでしょうか。さらに、これらの式のうち、いくつかはコンソールに文字列が出力されますが、その場合はどのような文字列が出力されるでしょうか。また、このlet
x
=
の右側の組み合わされた式は、それぞれがどの制御フロー式によってどのように組み合わされているかを答えてください。
let x = printf "hello"
let x = printf "hello"; printf "world"
let x = if 3.14 >= 0.0 then "positive" else
"negative"
let x = if 3.14 > 0.0 then printf "positive";1
else printf "negative";-1
let x = if 3.14 > 0.0 then ();1 else
();-1
int
型の識別子x
が与えられたとき、このx
の値が奇数ならば"odd"
、偶数ならば"even"
というstring
型の値を返すmatch式を書いてください。(剰余を計算するには%
演算子を使います)
(Fizz-Buzz問題(簡易版))int
型の識別子x
が与えられたとき、x
の値に応じて以下のような動作をする式を書いてください。
3の倍数のときはFizzをコンソールに出力
5の倍数のときはBuzzをコンソールに出力
3と5の倍数のときはFizzとBuzzをコンソールに出力
その他の場合はx
の値をコンソールに出力
パターンマッチ(pattern match)とは、与えられた値をパターン(pattern)とよばれる式へ適合させる動作のことです。その値がパターンに適合した場合を成功とよび、適合しなかった場合を失敗とよびます。パターンマッチは、大まかには以下の手順に従って実行されます。
与えられた
値のデータの構造と、パターンが表現するデータの構造が同じ形をしているか調べる。
同じ形でなければパターンマッチは失敗する。成功した場合、パターン内にあらわれる各識別子に、x
内部の値を束縛する。
これだけだとわかりにくいので、いくつかの例を示します。
match
式のところで使用したパターンは、定数パターン(constant
pattern)とよばれるもので、単純に与えられた値とリテラルが同じ値かを調べます。
> let x = 5;; val x : int > match x with | 3 -> printf "Three\n" | 5 -> printf "Five\n" | _ -> printf "Otherwise\n";; Five val it : unit = ()
この例は3つのルールをもっており、各ルールの->
の左側がパターンを表しています。つまりこの場合、3
、5
、_
がそれぞれパターンです。そして3
と5
が定数パターンになります。文字列の場合でも定数パターンを使うことができます。
> match "Five" with
| "Three" -> 3
| "Five" -> 5
| _ -> 0;;
val it : int = 5
また、以下のように複数のリテラルを並べて書くこともできます。
> match 1uy with
| 0uy | 1uy | 2uy -> printfn "less than 3"
| _ -> printfn "greater than or equal to 3";;
less than 3
val it : unit = ()
パターンには、リスト、配列、レコード、タプルなどの複合型を使うこともできます。
> match [2; 4; 6] with | [1;3;5] -> "odds" | [2;4;6] -> "evens" | _ -> "unknown";; // リストのパターン val it : string = "evens" > match 1,"one" with | 0,"zero" -> 0 | 1,"one" -> 1 | 2,"two" -> 2 | _ -> -1;; // タプルのパターン val it : int = 1
複合型のパターン内でワイルドカードパターンを使うと、より柔軟なパターンを表現することができます。
> match [3.14; 3.1; 0.8] with | [_] -> "1 element" | [_;_] -> "2 elements" | [_;_;_] -> "3 elements" | _ -> "4 or more elements";; // 3要素のリストにマッチする val it : string = "3 elements" > match [32; 2; 1] with | [2;_;_] -> "found 2 at 1st" | [_;2;_] -> "found 2 at 2nd" | [_;_;2] -> "found 2 at 3rd" | _ -> "not found";; // 2番目に2があるリストにマッチする val it : string = "found 2 at 2nd"
パターン内で識別子を使うこともできます。まず簡単な例を見てみましょう。
> match [1;2;3] with
| [x] -> x
| [x;y] -> x+y
| [x;y;z] -> x+y+z
| _ -> -1;;
val it : int = 6
パターン内の識別子にはマッチした要素が束縛されます。このようなパターンを変数束縛パターン(variable binding pattern)といいます。ただし、パターン内の識別子がdiscriminated unionの値のような定数値をあらわす場合は、定数パターンとして動作します。
> type Number = Zero | One | Two | Others;; type Number = | Zero | One | Two | Others > match Two with | Zero -> 0 | One -> 1 | Two -> 2 | Others -> -1;; val it : int = 2
また、リストに対する変数束縛パターンでは、次のようなものがよく使われます。
> match [5;4;3;2;1] with // 先頭とその残りに対してxとxsを束縛 | x::xs -> printfn "1st element is %d, tail is %s" x (any_to_string xs) | _ -> printfn "empty";; 1st element is 5, tail is [4; 3; 2; 1] val it : unit = () > match [5;4;3;2;1] with // 先頭とその次とその残りに対してxとyとxsを束縛 | x::y::xs -> printfn "1st element is %d, 2nd element is %d, tail is %s" x y (any_to_string xs) | _ -> printfn "empty" 1st element is 5, 2nd element is 4, tail is [3; 2; 1] val it : unit = () > match [] with // リストが空の場合(1番目のパターンマッチは失敗し、ワイルドカードパターンのほうにマッチする) | x::xs -> printfn "1st element is %d, tail length is %s" x (any_to_string xs) | _ -> printfn "list is empty" list is empty val it : unit = ()
ここでは、::
を使ってリストを分解しています。::
という演算子は前章のリストのところでも出てきましたが、そこではリストの先頭に要素を1つ追加するという動作をしていました。しかし、パターンマッチではその逆の動作をします。すなわち、::
を含むパターンへのマッチが成功した場合、リストは「先頭の1要素」と「その残りのリスト」に分解され、同時にそれぞれに識別子が束縛されます。
discriminated unionとパターンマッチの組み合わせは非常によく使われます。その中でも特に便利なのが、パラメータをもつdiscriminated unionと組み合わせた場合です。
> type figure = Point | Circle of int | Rectangle of int * int;; // 点または円または四角形を表すdiscriminated union type figure = | Point | Circle of int | Rectangle of int * int > match (Circle 2.0) with | Point -> 0.0 | Circle r -> r * r * 3.14 | Rectangle (w,h) -> w * h;; val it : float = 12.56
この組み合わせは非常に強力な表現力を持ちます。
asパターン(as pattern)を使うと、ある値に対してパターンマッチが成功した場合、その値全体を識別子に束縛することができます。言葉ではわかりにくいので例を見てみましょう。
> match [1.0; 2.0; 3.0] with
| [_;_;_] as t -> List.average t
| _ -> 0.0;;
val it : int = 2.0
あまりいい例が思いつかなかったのですが、この例ではリストが3要素だった場合にその平均をとり、それ以外は0を返します。あまり重要な構文ではありませんが、たまにあると便利な構文です。
ガード付きパターンルール(guarded pattern)を使うと、より柔軟に条件を指定することができます。
> match (5,3) with
| (x,y) when x < 0 -> -1
| (x,y) when x > 0 -> 1
| _ -> 0;;
val it : int = 1
この例では、タプルのパターンの後にwhen
がついています。このwhen
の後の条件は、ガード(guard)とよばれるものです。まず値が(x,y)
にマッチするかチェックして、チェックに成功するとこのガードによる検査が行われます。値がマッチしてさらにガード条件もtrue
になって、はじめてパターンマッチは成功します。
ここまでは、パターンマッチをmatch
式の中でを使ってきましたが、let
束縛の中で使うこともできます。ここまでのmatch
式でのパターンマッチは、主にデータの中身に応じて処理を分岐するのが目的でした。一方、let
束縛の中でパターンマッチを使うと、識別子の束縛や値の抽出を非常にスマートに書くことができます。
> let x,y = 3,5;; // 2要素のタプルのパターンマッチ&束縛 val y : int val x : int > x;; val it : int = 3 > y;; val it : int = 5 > let x,y = 3,5,7;; // パターンマッチに失敗 let x,y = 3,5,7;; ----------^^^^^^ stdin(13,11): error FS0001: Type mismatch. Expecting a 'a * 'b but given a 'a * 'b * 'c. The tuples have differing lengths of 2 and 3.
まず一番最初の入力では、3,5
というタプルをx,y
というパターンにマッチさせ、識別子の束縛も行っています。これにより、2つの識別子の束縛を一度に実行しています。
1月~12月を表す値が与えられたときに、その月の日数が31未満ならば「less than 31
days」と表示し、31日ならば「31 days」と表示し、範囲外の値ならば「invalid
month」と表示するmatch
式を、定数パターンを使って書いてください。
1と同じ問題を、今度はガード付きパターンを使って書いてください。
前節の練習問題のFizz-Buzz問題(簡易版)をガード付きパターンを使って書いてください。
名前付きパターンの説明で使ったfigureの値を受け取り、その値がPoint
ならば「Point」、Circle
ならば「Circle」、Rectangle
ならば「Rectangle」と表示するmatch式をガード付きパターンを使って書いてください。
ただし、Circle
とRectangle
の場合、その面積が0ならば点とみなして、「Point」と表示するようにしてください。ここではCircle
のパラメータは円の半径、Rectangle
のパラメータはそれぞれ幅と高さをあらわすとします。
4と同じ問題を名前付きパターンのみを使って書いてください。