open netshop

Chapter 1. 基本的な構文

Table of Contents

1. 式
1.1. 複合式
1.2. 条件式
1.3. match式
1.4. 練習問題
2. パターンマッチ
2.1. 定数パターン
2.2. 複合型のパターン
2.3. 名前付きパターン
2.4. asパターン
2.5. ガード付きパターン
2.6. let束縛とパターンマッチ
2.7. 練習問題

1. 式

関数型言語では、式(expression)がプログラムの基本要素となります。ここまでの章でも、さまざまな式を使ってきました。たとえば前の章の最後で紹介した、束縛式もひとつの式であり、3"Hello"といったリテラルも、単純定数式(simple constant expression)というひとつの立派な式です。

このように、式には多くの種類が存在しますが、この章ではそれらのうち制御フロー式(control flow expression)という種類のものを紹介します。誤解を恐れずにいうと、F#のプログラムはすべてが式であり、F#のプログラムを作ることは、複数の小さな式を組み合せて1つの巨大な式を作ることと等しいといえます。この章で紹介する制御フロー式は、複数の式を組み合わせるための糊のひとつです。

1.1. 複合式

複合式(compound expression)はセミコロンで区切られた複数の式を左から順番に評価し、一番右の式の評価結果を式全体の値とします。一番右以外の式の評価結果は捨てられてしまいます。構文は以下のとおりです。複合式は、文法的にもれっきとしたひとつの式であることに注意してください。

expr1 ; expr2

> printf "Hello,\n"; printf "world\n";; // Helloを出力してからworldを出力する
Hello,
world

軽量構文ではセミコロンのかわりに改行を使うことができます。

> printf "Hello,\n"
printf "world\n";;
Hello,
world

Note

軽量構文ではインデントは重要な意味を持ちます。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 = ()

1.2. 条件式

条件式(conditional expression)は、もっとも基本的な条件分岐構文です。条件式の構文は以下のとおりです。

if boolean_cond1 then expr1 [elif boolean_cond2 then expr2]* [else expr3]

boolean_cond1boolean_cond2bool値の式であり、expr1expr2expr3はすべて同じ型の式である必要があります。elseを省略した場合はexpr1expr2unit型でなければなりません。具体的な例をみてみましょう。

> 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.
      

最後の入力のエラーは、expr1expr3の型が異なっているために発生しています。

1.3. match式

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式が強力であるゆえんは、ひとえにパターンマッチの強力さによるものです。この例では、35_という値を比較するだけの単純なパターンしか使っていないので、その強力さはわかりませんが、実際には非常に多くのバリエーションがあります。それらについて次節で説明します。

1.4. 練習問題

  1. 以下の式は、すべてlet x =の右側に制御フロー式によって組み合わされた1つの式があり、この式を実行すると、xにその式の最終的な値が入ります。それぞれxにはどのような値が入るでしょうか。さらに、これらの式のうち、いくつかはコンソールに文字列が出力されますが、その場合はどのような文字列が出力されるでしょうか。また、このlet x =の右側の組み合わされた式は、それぞれがどの制御フロー式によってどのように組み合わされているかを答えてください。

    1. let x = printf "hello"

    2. let x = printf "hello"; printf "world"

    3. let x = if 3.14 >= 0.0 then "positive" else "negative"

    4. let x = if 3.14 > 0.0 then printf "positive";1 else printf "negative";-1

    5. let x = if 3.14 > 0.0 then ();1 else ();-1

  2. int型の識別子xが与えられたとき、このxの値が奇数ならば"odd"、偶数ならば"even"というstring型の値を返すmatch式を書いてください。(剰余を計算するには%演算子を使います)

  3. (Fizz-Buzz問題(簡易版))int型の識別子xが与えられたとき、xの値に応じて以下のような動作をする式を書いてください。

    • 3の倍数のときはFizzをコンソールに出力

    • 5の倍数のときはBuzzをコンソールに出力

    • 3と5の倍数のときはFizzとBuzzをコンソールに出力

    • その他の場合はxの値をコンソールに出力

2. パターンマッチ

パターンマッチ(pattern match)とは、与えられた値をパターン(pattern)とよばれる式へ適合させる動作のことです。その値がパターンに適合した場合を成功とよび、適合しなかった場合を失敗とよびます。パターンマッチは、大まかには以下の手順に従って実行されます。

  1. 与えられた値のデータの構造と、パターンが表現するデータの構造が同じ形をしているか調べる。

  2. 同じ形でなければパターンマッチは失敗する。成功した場合、パターン内にあらわれる各識別子に、x内部の値を束縛する。

これだけだとわかりにくいので、いくつかの例を示します。

2.1. 定数パターン

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つのルールをもっており、各ルールの->の左側がパターンを表しています。つまりこの場合、35_がそれぞれパターンです。そして35が定数パターンになります。文字列の場合でも定数パターンを使うことができます。

> 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 = ()

2.2. 複合型のパターン

パターンには、リスト、配列、レコード、タプルなどの複合型を使うこともできます。

> 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"

2.3. 名前付きパターン

パターン内で識別子を使うこともできます。まず簡単な例を見てみましょう。

> 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

この組み合わせは非常に強力な表現力を持ちます。

2.4. asパターン

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を返します。あまり重要な構文ではありませんが、たまにあると便利な構文です。

2.5. ガード付きパターン

ガード付きパターンルール(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になって、はじめてパターンマッチは成功します。

2.6. let束縛とパターンマッチ

ここまでは、パターンマッチを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つの識別子の束縛を一度に実行しています。

2.7. 練習問題

  1. 1月~12月を表す値が与えられたときに、その月の日数が31未満ならば「less than 31 days」と表示し、31日ならば「31 days」と表示し、範囲外の値ならば「invalid month」と表示するmatch式を、定数パターンを使って書いてください。

  2. 1と同じ問題を、今度はガード付きパターンを使って書いてください。

  3. 前節の練習問題のFizz-Buzz問題(簡易版)をガード付きパターンを使って書いてください。

  4. 名前付きパターンの説明で使ったfigureの値を受け取り、その値がPointならば「Point」、Circleならば「Circle」、Rectangleならば「Rectangle」と表示するmatch式をガード付きパターンを使って書いてください。

    ただし、CircleRectangleの場合、その面積が0ならば点とみなして、「Point」と表示するようにしてください。ここではCircleのパラメータは円の半径、Rectangleのパラメータはそれぞれ幅と高さをあらわすとします。

  5. 4と同じ問題を名前付きパターンのみを使って書いてください。

inserted by FC2 system