open netshop

Chapter 1. 基本的な型と演算

Table of Contents

1. 単純型
1.1. int
1.2. float
1.3. char
1.4. string
1.5. bool
1.6. 多倍長数型
1.7. unit
1.8. 単純型のまとめ
1.9. 練習問題
2. 複合型
2.1. タプル
2.2. レコード
2.3. リスト
2.4. 配列
2.5. Discriminated union
2.6. 練習問題
3. 識別子
3.1. 識別子への束縛
3.2. 識別子名の命名規則
3.3. 練習問題

まずここでは、F#で扱える単純型と演算について解説します。そしてこの章の最後では、識別子という(ほかの言語における)変数に似た概念について説明します。

1. 単純型

1.1. int

32ビットの整数を表現することができる型です。int型には算術演算子やビット演算子や比較演算子が定義されています。

> 3;; // リテラル
val it : int = 3
> 0xFF;; // リテラル(16進表記)
val it : int = 255
> 1 + 2;; // 算術演算
val it : int = 3
> 2 <<< 3;; // ビット演算
val it : int = 16
> 10 = 5;; // 比較演算
val it : bool = false
> 10 > 3;; // 比較演算
val it : bool = true

Note

リテラルとはプログラムコードの中に直接記述される数値や文字列などの値のことです。

1.2. float

64ビットの浮動小数点数を表現することができる型です。int型と同様、算術演算子と比較演算子が定義されていますが、ビット演算子は定義されていません。floatリテラルの表記方法は、一般的な小数の表現に加えて指数表記も可能です。

> 3.14;; // リテラル
val it : float = 3.14

> 6.02e23;; // リテラル(指数表記)
val it : float = 6.02e+23

> 3.14 + 1.2;; // 算術演算
val it : float = 4.34

> 1.0 = 2.0;; // 比較演算
val it : bool = false

> 2.0 < 5.5;; // 比較演算
val it : bool = true

> 2.0 <<< 3;; // ビット演算(定義されていないのでエラー)

  2.0 <<< 3;;
  ^^^^

stdin(91,1): error FS0001: The type 'float' does not support any operators named '<<<'.        

F#の数値型は暗黙的に型変換されないため、float型とint型同士の算術演算や比較演算を直接行うことはできません。それらを行うためには、明示的に型変換を行う必要があります。

> 1.0 + 2;;
1.0 + 2;;
------^^

stdin(12,5): error FS0001: The type 'int' does not match the type 'float'.

> 1.0 + float 2;; // 型変換演算子floatを使って2を2.0に変換
val it : float = 3.0

Note

型名であるfloatと型変換演算子floatは両方とも同じ名前ですが、片方は型名で片方は演算子名です。これらはたまたま同じ名前なだけで、構文上では厳密に区別されます。実際、それぞれの完全修飾名はFSharp.Core.floatFSharp.Core.Operators.floatで、全く違うものです。

1.3. char

char型は16ビットのUnicode文字を表現することができます。文字リテラルの表現には、C#などのようにエスケープシーケンスを用いることができます。

> 'x';; // アルファベット
val it : char = 'x'

> 'あ';; // ひらがな
val it : char = 'あ'

> '山';; // 漢字
val it : char = '山'

> '\n';; // エスケープシーケンス
val it : char = '\n'

> '\026';; // 三連文字(スペース文字の文字コード)
val it : char = ' '

> '\u0061';; // 文字コードによる表現
val it : char = 'a'        

型変換演算子を用いると、char型からint型などへ変換することもできます。

> int 'あ';; // 'あ'をint型に変換
val it : int = 12354

> '\u3042';; // 12354の16進表現は0x3042
val it : char = 'あ'        

Tip

上の例ではコンソールに日本語の文字を入力していますが、コンソール版のF#インタプリタはマルチバイト文字を正しく処理できません。F#インタプリタ上で日本語を使いたい場合は、Visual StudioのF#インタプリタを使用してください。

1.4. string

string型はchar型の文字で構成される文字列、すなわちUnicode文字列を表現することができます。char型と同様に文字列リテラルにはエスケープシーケンスを用いることができますが、それに加えて逐語的(verbatim)表現や32ビットのUnicode文字表現などを用いることができます。逐語的表現は@""で囲まれた文字列をそのまま解釈するもので、\が現れてもエスケープシーケンスとして認識しませんし、改行もそのまま改行文字として解釈します。

> "Hello, world!";; // アルファベット文字列
val it : string = "Hello, world!"ことが

> "こんにちは";; // 日本語文字列
val it : string = "こんにちは"

> "世界\n";; // エスケープシーケンス
val it : string = "世界\n"

> "\U00003042いうえお";; // 32ビットの文字コードによる表現
val it : string = "あいうえお"

> @"C:\Program Files\FSharp";; // 逐語的表現
val it : string = "C:\Program Files\FSharp"

> @"Hello
F#";; // 逐語的表現(改行を含む)
val it : string = "Hello\nF#"        

string型にはいくつかの演算子やプロパティが定義されています。

> "abcdef".[3];;
val it : char = 'd'

> "abc" + "def";;
val it : string = "abcdef"

> "abcあいう".Length;;
val it : int = 6        

1.5. bool

bool型は真理値を表現することができます。C#などと同様、さまざまな論理演算子が定義されています。

> true;; // リテラル
val it : bool = true

> not false;; // 否定
val it : bool = true

> 1 > 3;; // 比較演算子
val it : bool = false

> true <> false;; // 比較演算子
val it : bool = true

> (1 < 3) && (5.0 > 2.0);; // 論理積算子
val it : bool = true        

論理積演算子&&と論理和演算子||は、両方とも短絡評価(short-circuit evaluation)を行います。たとえば、a && bという式があった場合、afalseだということがわかれば、bを評価しなくても式全体の値はfalseだということが即座にわかるので、その場合F#はbの評価を行いません。同様にしてa || bの場合、atrueのとわかったらF#はbの評価を行いません。

Note

評価(evaluation)とはラムダ計算論およびプログラミング言語論の用語で、簡単にいってしまうと、式が実行されてその結果として1つの値が返されることをいいます。たとえば2 + 3という式を評価すると、足し算が行われて結果として5という値が返されます。また、printf "hello"という式を評価すると、画面にhelloという文字列が出力され、()という値(後述するunit型の値)が返されます。

ここで重要なのは、式を評価すると必ず何らかの値が返ることです。2 + 3の例では5という値が返り、printfのように値が特に意味を持たない場合でも、必ず()という値が返されました。ところが式(expression)ではなく文(statement)の場合は、そもそも値が返されることはありません。「基本的な構文」の章で説明しますが、関数型言語は基本的に文を持たず全てが式なのです。

評価という概念は奥が深く、ある式がいくつかの小さな式から構成されている場合、それらの小さな式をどのような順番で評価するかによって、プログラムの実行結果が変わる場合があります。式を評価する方法にはさまざまな名前がついており、それらは評価戦略と呼ばれます。評価戦略は言語ごとにそれぞれ違うものが採用されており、F#では正格評価という評価戦略が採られています。ただし発展編で紹介するように、遅延評価をエミュレートすることもできます。

1.6. 多倍長数型

多倍長数型は、メモリが許す限りいくらでも大きい整数と細かい精度の有理数を表現することができます。

多倍長整数型bigintと多倍長有理数型bignumのリテラルは、数値の最後にINをつけることで表現します。

Warning

F# 1.9.6.16(Visual Studio 2010 beta 1)からは、bignumはF# PowerPackというF#の付属ライブラリに移動しました。F# PowerPackは、以下の場所からダウンロードすることができます。

Microsoft F# PowerPack for .NET 4.0 Beta1

上のファイルを適当な場所に展開して、F# インタプリタ上で以下の1行を入力すると、bignumを利用できるようになるはずです。

> #r "FSharp.PowerPack.dll";;
> 12000000000000000000000000000000000;; // int型では表現できないためエラー

12000000000000000000000000000000000;;
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

stdin(7,1): error FS0191: This number is outside the allowable range for 32-bit signed integers.
> 12000000000000000000000000000000000I;; // bigint型のリテラル
val it : bigint = 12000000000000000000000000000000000I

> 5N;;
val it : bignum = 5 {Denominator = 1;
                     IsNegative = false;
                     IsPositive = true;
                     Numerator = 5;
                     Sign = 1;}

> 32N/8N;; // 有理数
val it : Math.BigNum = 4 {Denominator = 1;
                          IsNegative = false;
                          IsPositive = true;
                          Numerator = 4;
                          Sign = 1;}

> 8N/32N;; // 有理数(既約分数で出力される)
val it : Math.BigNum = 1/4 {Denominator = 4;
                            IsNegative = false;
                            IsPositive = true;
                            Numerator = 1;
                            Sign = 1;}

Note

.NET 3.0までは多倍長数型のサポートがありませんでしたが、.NET 4.0からはSystem.Numeric.BigIntegerというクラスによって多倍長数型がサポートされるようになりました。

F#では、.NET 4.0が利用できる場合はBigIntegerクラスを利用し、利用できない場合はFSharp.Core.dllにある自前の実装を利用します。

1.7. unit

unit型は()という1つの値のみを表現することができる型です。この値はC#のvoidのようなもので、値自体に特に意味はありません。F#における関数の呼び出しには、必ず何らかの引数を渡したり、何らかの値を返したりしなければなりません。一般的に引数や戻り値が特に意味を持たない場合は、この値をダミーとして受け取ったり返したりします。たとえばprintfによる文字出力は()を返します。

> printf "hello, world!\n";;
hello, world!
val it : unit = ()      

1.8. 単純型のまとめ

今までは主要な型を紹介してきましたが、単純型をまとめると以下のような分類になります。

Table 1.1. 単純型の種類

種類詳しい種類
数値型整数型
小数型
多倍長型(bigint, bignum)
文字型/文字列型文字型(char)
文字列型(string)
その他unit, bool



この中で、整数型はさらに細かく以下のような種類があります。

Table 1.2. 数値型の種類

表現型名ビット幅表現できる範囲リテラルの例
符号つき整数sbyte8-128~1270y,19y,0xFFy
int1616-32,768~32,7670s,19s,0x0800s
int32/int32-2,147,483,648~2,147,483,6470,19,0x0800,0b0001
int6464-9,223,372,036,854,775,808~9,223,372,036,854,775,8070L,19L,0x0800L
nativeintマシン依存マシン依存0n,19n,0x0800n
符号なし整数byte80~2550uy,19uy,0xFFuy
uint16160~65,5350us,19us,0x0800us
uint32320~4,294,967,2950u,19u,0x0800u
uint64640~18,446,744,073,709,551,6150UL,19UL,0x0800UL
unativeintマシン依存マシン依存0un,19un,0x0800un



同様に小数型には以下の種類があります。

Table 1.3. 小数型の種類

表現型名ビット幅表現できる範囲有効桁数リテラルの例
2進浮動小数点数float32/single32±1.5×10-45~±3.4××10387桁3.14f, 2.718F, 11.5e3f, 0x01lf
float/double64±5.0×10-324~±1.7×1030815~16桁3.14, 115e-3, 0x01LF
10進浮動小数点数decimal128±1.0×10-28~±7.9×10102828~29桁3.14m, 11.5M



todo: 算術演算子、ビット演算子などについてまとめる

1.9. 練習問題

  1. 以下の式を評価した結果の型と値を予想したあとに、自分でその結果を確かめてください。エラーになる場合はその理由を考えてみてください。

    1. 1 + 3

    2. 13 + 23.0

    3. 3y * sbyte 2

    4. int 23.5 + 13

    5. 10.0e3f / 10.0e1f

    6. 0uy - 1uy

    7. 'a' + 'b'

    8. char (uint16 '0' + 1us)

    9. "abc".[1] = 'b'

  2. 大文字のアルファベット「A」~「Z」はASCIIコードでは0x41~0x5Aで表され、小文字のアルファベット「a」~「z」は0x61~0x7Aで表されます。このような関係から、6ビット目の1/0を切り替えることで、簡易に大文字と小文字の変換をすることができます。

    型変換演算子とビット演算子を使って、文字リテラル'A'char型のアルファベット'a'に変換する式を書いてください。

  3. 次の式がfalseと評価されてしまう理由を答えてください。

    > 0.1f + 0.1f + 0.1f = 0.3f;;
    val it : bool = false          

    Note

    わからない方は浮動小数点数の規格IEEE 754について調べてみてください。

2. 複合型

複合型は、1つ以上の単純型を組み合わせて構成する型です。ここではその中からもっとも基本的な複合型である、タプル、レコード、リスト、配列、discriminated unionを紹介します。タプルとレコードとdiscriminated unionは決まった要素数をもち、リストと配列は可変の要素数をもつ複合型です。

2.1. タプル

タプルは複数の基本型をペアにしたデータ型です。例えば、intstringstringboolintのように複数の基本型をペアにします。作り方は簡単で、ただデータを並べたものをカンマで区切るだけです。しかし演算子の優先順位の問題でうまく認識されないケースが場合があるので、タプルを書くときは念のために全体を括弧で囲うようにしておけば間違いは減らせます。

> 1,"hello";; // int型とstring型のペア(括弧なし)
val it : int * string = (1, "hello")

> (1,"hello");; // int型とstring型のペア(括弧あり)
val it : int * string = (1, "hello")

> ("Taro",23,true);; // string型とint型とbool型のペア
val it : string * int * bool = ("Taro", 23, true)

> (0.0,0.0);; // float型とfloat型のペア
val it : float * float = (0.0, 0.0)

> ((1,"Hello"),0.0f);; // int型とstring型のペアとfloat32型のペア
val it : (int * string) * float32 = ((1, "Hello"), 0.0f)

タプルの型名は、タプルを構成する型名を*で区切って並べたものです。たとえばintstringならば、型名はint * stringとなります。

タプルは関数型言語ではよく用いられる型で、たとえばxy座標の点を表現したり、関数の戻り値として複数の情報を返したいときなどに適しています。特に2つ組のタプルはよく使うため、特別に以下のように便利な関数fstsndが定義されています。

> fst ("abc",35);; // 1番目の要素の値を取り出す
val it : string = "abc"

> snd (0.0,3.1415);; // 2番目の要素の値を取り出す
val it : float = 3.1415

> fst ((1,2),5);; // 1番目の要素の値を取り出す
val it : int * int = (1, 2)

> fst ("abc",3,5);; // 3つ組のタプルに対してはエラー

fst ("abc",3,5);;
-----^^^^^^^^^^

stdin(23,6): error FS0001: Type mismatch. Expecting a
'a * 'b
but given a
'a * 'b * 'c.
The tuples have differing lengths of 2 and 3.
      

Note

(このNoteはわからなければ読み飛ばしてください)

タプルの値と型は、数学的には順序対と直積集合の関係にあたります。数学では2次元空間上の点は、たとえばのように表します。順序対はタプルの値(3.0, 5.8)であり、はタプルの型float * floatに相当します。

2.2. レコード

レコードはC#の構造体に似たものです。後で紹介するように、F#からも.NETの構造体を利用することはできますが、レコード型を使うと構造体よりも簡潔な表現ができます。

> type Point = { X : int; Y : int };; // Pointというレコード型を定義

type Point =
  {X: int;
   Y: int;}

> { X = 0; Y = 10 };; // インスタンスを生成
val it : Point = {X = 0;
                  Y = 10;}

> { Y = 0; X = 10 };; // 値を割り当てる順番は自由
val it : Point = {X = 10;
                  Y = 0;}

レコードの定義は、この例のように複数のメンバをセミコロンで区切り、全体を{}で囲います。軽量構文の場合は、各メンバの宣言のあとに改行を入れることでセミコロンを省略することができます。レコードのインスタンスを作るには、すべてのメンバに対してメンバ名 = という形で値を割り当て、全体を{}で囲います。値を割り当てる際の順番は特に関係ありません。

もし、全く同じメンバを持つレコード型が2つあるときにインスタンスを作ろうとすると、後から定義されたほうのレコードのインスタンスが生成されます。レコードの型を明示的に指定するには、メンバの前にレコードの型名を修飾します。

> type Position = { X : int; Y : int };;

type Position =
  {X: int;
   Y: int;}

> { X = 0; Y = 10 };; // Pointではなく、後から定義されたPositionとして認識される
val it : Position = {X = 0;
                     Y = 10;}

> { Point.X = 0; Point.Y = 10 };; // Pointであることを明示的に指定する
val it : Point = {X = 0;
                  Y = 10;}

コピー&更新レコード式(copy and update record expression)という構文を使うと、新しいインスタンスを生成する際に、既存のインスタンスの値をそのままコピーして、一部の値だけを書き換えたインスタンスを作ることができます。この構文は、インスタンス生成時に、先頭にコピー元のインスタンスの名前とwithを付け加えることで利用します。

次の例はコピー&更新レコード式の例です。ここではインスタンスに名前をつけるために、let束縛というものを使っていますが、これについては後で詳しく説明します。ここではとりえあず、単なる変数の定義だと考えておいてください。

まず、最初の入力でRectレコードを定義します。次に、2番目の入力でr1という名前のインスタンスをつくり、3番目の入力でコピー&更新レコード式により新たなインスタンスr2を作ります。この場合は、XYWidthの値をそのまま利用して、HeightNameだけを新たに与えています。そして、最後の入力でr2の中身を確認しています。

> type Rect =
    { X : int; Y : int; Width : int; Height : int
      Name : string };;

type Rect =
  {X: int;
   Y: int;
   Width: int;
   Height: int;
   Name: string;}

> let r1 = { X=0; Y=0; Width=10; Height=10; Name="Rect1" };;

val r1 : Rect = {X = 0;
                 Y = 0;
                 Width = 10;
                 Height = 10;
                 Name = "Rect1";}

> let r2 = { r1 with Height=100; Name="Rect2" };; // コピー&更新レコード式によりr1の一部の値を再利用

val r2 : Rect = {X = 0;
                 Y = 0;
                 Width = 10;
                 Height = 100;
                 Name = "Rect2";}

> r2;;
val it : Rect = {X = 0;
                 Y = 0;
                 Width = 10;
                 Height = 100;
                 Name = "Rect2";}

Note

(このノートはわからなければ読み飛ばしてください)

タプルが順序対に相当するのに対して、レコードは非順序対に相当します。なぜならば、タプルでは値の並び順が重要な意味をもちましたが、レコードでは本質的に順番には意味がないと考えられるからです(実装レベルでは話は別ですが)。

2.3. リスト

リストはある特定の型の値をいくつも格納することができる可変長のデータ構造ですが、このデータ構造は関数型言語特有のもので、.NET Frameworkの従来のコレクションクラスの中に相当するものはありません。リストは本質的には、以下の図に示す単方向リンクトリスト(単方向連結リスト)として実装されているのですが、System.Collections.Genericにあるような従来のデータ構造とは大きく異なる性質を持っています。その性質とは「不変性」です。この性質の関する詳しい説明や、この性質を利用したデータ処理については後の「データ処理」の章で詳しく説明するので、ここではとりあえず基本的な扱い方のみを説明します。

リストの「特定の型を格納する可変長のデータ構造」という性質だけを見ると、一見System.Collections.Generic.LinkedListクラスと同じように見えますが、こちらは双方向リンクトリストとして実装されており、しかも各要素の値を書き換えることができるため(すなわち不変でない)、また異なる性質をもつデータ構造となります。

このリストというデータ構造は、関数型プログラミングでよく利用されます。その理由のひとつとして、リストは再帰的な処理に向いているという点が挙げられるのですが、詳しくは後の章に譲ることにします。

リストの一番簡単な作り方は、同じ型のデータ列をセミコロンで区切って、[]で囲むだけです。

> [1;2;3;-1;5232];; // int型のリスト
val it : int list = [1; 2; 3; -1; 5232]

> ["hello";"world"];; // string型のリスト
val it : string list = ["hello"; "world"]

> [1;2;3;3.1415];; // 異なる型のリストは作れない

[1;2;3;3.1415];;
-------^^^^^^^

stdin(8,8): error FS0001: This expression has type
  float
but is here used with type
  int.
      

リストの型名は、上の例をみるとわかるように、格納している型の名前のあとにlistというキーワードをつけます。リストを作る記法には、以下のように先頭の値と終端の値を指定するだけでその間を埋めてくれる便利なものあります。この記法は範囲式(range expression)と呼ばれ、配列などの他のコレクション型でも利用することができます。

> [1..5];; // 1から5までのリスト(1ずつ増える)
val it : int list = [1; 2; 3; 4; 5]

> [1..2..5];; // 1から5までのリスト(2ずつ増える)
val it : int list = [1; 3; 5]      

Note

タプルもリストも複数の値を格納できるデータ構造なので、この2つの違いがわからずに混乱している方もいるかもしれません。しかしこれらは全く違うものです。C#でいうとタプルは構造体に相当し、リストはList<T>クラスに相当すると考えてください。

リストは関数型プログラミングにおいて中心的なデータ構造であり、多くの関数が用意されているのですが、それを説明するには後述する高階関数というものについて理解しておく必要があります。ここでは、非常に基本的な関数と演算子のみを紹介するにとどめ、その他の説明は後回しにします。

> List.hd [1;2;3];; // 先頭要素を取り出す
val it : int = 1

> List.tl [1;2;3];; // 末尾を取り出す
val it : int list = [2;3];;

> 1 :: [2;3;4];; // 先頭に要素を追加
val it : int list = [1; 2; 3; 4]

> 1 :: 2 :: 3 :: [];; // 空のリストに先頭から次々と要素を追加
val it : int list = [1; 2; 3]

> [1;2;3] @ [4;5;6];; // 連結
val it : int list = [1; 2; 3; 4; 5; 6]

> List.length [1;2;3];; // 長さを求める
val it : int = 3

> List.average [10.0;20.0;30.0;40.0];; // リストの要素の平均値を計算する
val it : float = 25.0

> List.zip [1;2;3] ['a';'b';'c'];; // 2つの異なるリストのそれぞれの要素をタプルでまとめる
val it : (int * char) list = [(1, 'a'); (2, 'b'); (3, 'c')]

> any_to_string [1;2;3];; // リストの文字列表現を得る
val it : string = "[1; 2; 3]"

最後のany_to_stringは、リスト以外のさまざまな型に対しても適用することができ、適切な文字列表現を返してくれる便利な関数です。

2.4. 配列

F#でもC#と同じように配列を扱うことができます。配列の作り方は、同じ型のデータ列をセミコロンで区切って、[||]で囲むだけです。

> [|1;2;3|];;
val it : int array = [|1; 2; 3|]

配列の型名は、上の例をみるとわかるように、格納されている型名のあとにarrayというキーワードがつきます。配列には以下のような関数と演算子が定義されています。

> [|1;2;3|].[0];; // 0番目の要素にアクセス
val it : int = 1

> [|"Hello";"World!"|].[1];; // 1番目の要素にアクセス
val it : string = "World!"

> let x = [|"Hello";"World!"|];; // 配列の値にxという名前をつける
val x : string array = [|"Hello"; "World!"|]

> x.[1];; // xが示す配列の1番目の要素にアクセス
val it : string = "World!"

> Array.append [|1;2;3|] [|4;5;6|];; // 二つの配列を連結した新しい配列を作る
val it : int [] = [|1; 2; 3; 4; 5; 6|]

> [|1..3|];; // 1から3までの値の配列を作る
val it : int array = [|1; 2; 3|]

> Array.concat [| [|1;2;3|]; [|4;5;6|] |];; // 配列内にある各配列を連結して1つの配列にする
val it : int [] = [|1; 2; 3; 4; 5; 6|]

> List.to_array [0..10];; // リストから配列を生成
val it : int array = [|0; 1; 2; 3; 4; 5; 6; 7; 8; 9; 10|]

> Array.to_list [|0..10|];; // 配列からリストを生成
val it : int list = [0; 1; 2; 3; 4; 5; 6; 7; 8; 9; 10]

> any_to_string [|0..5|];; // 配列の文字列表現を得る
val it : string = "[|0; 1; 2; 3; 4; 5|]"

リストと同様、配列にも多くの高階関数が用意されていますが、それらは後で紹介します。

2.5. Discriminated union

discriminated unionはプログラミング言語論的には代数的データ型(Abstract data type; ADT)と呼ばれるもので、近代的な関数型言語ならばたいていこの機能を備えています。

discriminated unionは複数の異なる値を包んでひとまとめに扱うことができます。しかし、その異なる値を同時に複数持つことはできず、必ずどれか1つだけのデータを持つことができます。例を見てみましょう。

> type sex = Male | Female;; // 男または女を表すdiscriminated union
type sex =
  | Male
  | Female

> Male;; // sex型に属するMaleという値
val it : sex = Male

> Female;; // sex型に属するFemaleという値
val it : sex = Female

> type figure = Point | Circle of int | Rectangle of int * int;; // 点または円または四角形を表すdiscriminated union
type figure =
  | Point
  | Circle of int
  | Rectangle of int * int

> Point;; // figure型に属するPointという値
val it : figure = Point

> Circle 10;; // figure型に属するCircleという値(半径10)
val it : figure = Circle 10

> Rectangle (3,10);; // figure型に属するRectangleという値(幅3、高さ10)
val it : figure = Rectangle (3,10)

> type tree =
  | Node of tree * tree
  | Leaf of int;; // 再帰的なdiscriminated union
type tree =
  | Node of tree * tree
  | Leaf of int

> Leaf 3;;
val it : tree = Leaf 3

> Node (Leaf 3, Leaf 0);;
val it : tree = Node (Leaf3,Leaf 0)

最初の例は、男か女という2つの値のうちどちらかをとりうるsex型を定義しており、次の例は点、円、四角形という3種類の値のうちどれかをとりうるfigure型を定義しています。

figure型をみるとわかるように、discriminated unionを構成するそれぞれの値にはパラメータを持たせることができます。たとえば、ここではCircle型にはint型、Rectangleにはint * int(int型とint型のタプル)を持たせ、それぞれ半径と幅・高さを表すように定義しました。

実際に自分で定義したdiscriminated unionの値を作り出すには、その値の名前を書きます。もし、その値がパラメータを持つ場合は、具体的なパラメータの値を与えます。

関数型言語におけるdiscriminated unionのような代数的データ型は、後で出てくるパターンマッチングという機能とあわせることで、非常に強力な表現力をもつようになります。特に条件分岐を非常に簡潔に記述することができます。

一番最後の例は、自身の中に自身の型を入れる再帰的なdiscriminated unionです。

Note

(このノートはわからなければ読み飛ばしてください)

discriminated unionは複数の異なる値を1つにまとめ、それらのうちどれか一つの値をとります。それを数学的に考えると、discriminated unionは互いに素な集合たちの和集合、すなわち直和であるということができます。

2.6. 練習問題

  1. 次のタプル値の型を答えてください。

    1. (10,"hello")

    2. (3.14,10uy,235m)

    3. ((),12345)

    4. (("abcde",10),1.4142)

    5. (("pi",3.1415),("e",2.71828))

  2. 以下のプログラムは、円を表現するCircleというレコード型を定義し、中心が(0,0)で半径が10である矩形のインスタンスを作ってr10という名前をつけています。これに対してコピー&更新レコード式を用いて、中心が(0,0)で半径が20である円を作ってください。

    > type Circle = { x:float; y:float; r:float; };;
    
    type Circle =
      {x: float;
       y: float;
       r: float;}
    
    > let r10 = { x=0.0; y=0.0; r=10.0 };;
    val r10 : Circle = {x = 0.0;
                       y = 0.0;
                       r = 10.0;}
    
  3. 矩形の左上の座標を表現するfloat * float型のメンバであるpositionと、幅と高さを表現するfloat * float型のメンバであるsizeという2つのメンバをもつレコード型Rectを定義してください。

  4. 次の式の実行結果を答えてください(4番目はちょっとした引っ掛け問題です)。

    1. Array.length [|0..99|]

    2. List.length [0..5..99]

    3. List.zip [0..3] ["This";"is";"a";"pen"]

    4. List.length [0,1,2,3]

    5. [0..1000..10]

    6. [|100..0|]

    7. [10..-3..-5]

    8. List.concat [ [1..3]; Array.to_list [|4..10|] ]

  5. 以下に指示にしたがって、複数個の重さのデータを表現できるWeightsという名前のdiscriminated unionを定義してください。

    • GramPoundという2つのメンバにより構成され、グラムかポンドという単位で重さを表現できる。

    • どちらのメンバも、floatのリストをパラメータとしてもつ。

    実際の使用例は以下のようになるはずです。

    > Gram [382.5; 319.2; 432.4; 298.0; 532.4];;
    val it : Weights = Gram [382.5; 319.2; 432.4; 298.0; 532.4]
    
    > Pound [1.4; 0.83; 0.914; 2.1; 1.55];;
    val it : Weights = Pound [1.4; 0.83; 0.914; 2.1; 1.55]

3. 識別子

現在主流であるC#などの命令型言語には変数(variable)という概念があり、よく「値を格納することができる箱」とたとえられます。一方、関数型言語にも識別子(identifier)という同じような概念があります。どちらも値に対して名前を結びつける役割をするものですが、それらには微妙な違いがあります。

関数型言語の識別子は、intなどの「通常の値」と「関数」を同列に扱うことができます。すなわち3"abc"などのリテラルと同じように関数も1つの値として扱います。プログラミング言語論のことばで表現すると、「関数を第一級オブジェクト(first class object)として扱うことができるのが関数型言語の特徴である」ということができます。

Note

第一級オブジェクトという言葉は人によって多少定義が異なりますが、だいたい次のような定義になっているようです。

  • 名前をつけることができる。

  • 関数の引数として渡したり、戻り値として返したりできる。

  • データ構造に格納できる。

たとえば、int型の3string型の"abc"などは上記の性質を満たしていることはすぐにわかるでしょう。関数型言語では関数自体が上記の性質を満たしており、普通の値として扱うことができます。この性質が関数型プログラミングでは非常に重要となります。

3.1. 識別子への束縛

変数では、ある変数に値を入れることを「変数に値を代入する(assign)」と表現しますが、識別子ではそのことを「値に識別子を束縛する(bind)」と表現します。F#で値を識別子に束縛するには、let束縛(let binding)を用いて以下のように記述します。

> let x = 10;;
val x : int

この例ではint型の10という値にxという識別子を束縛しています。代入のイメージは用意された箱に値を格納するというものなのに対し、束縛はある値に名札をつけるというイメージです。この表現のように、「代入」では変数が主体の表現をしますが、「束縛」では値が主体の表現をします。C/C++では、変数の宣言だけして初期化をしないと不定な値が入って思わぬバグの原因になったりしますが、F#ではそもそも構文的に不定な値は入ることはありえません。

let束縛中の=の右側には式を書くことができます。その場合、識別子には式を評価した結果の値が束縛されます。

> let result = 3 > 1;;
val result : bool      

ここでは3 > 1を評価した結果の値がresultに束縛されています。ここで注目してほしいのが、識別子resultの型が自動的にboolだと決定されていることです。C/C++では変数の宣言時には必ず型を指定しなければなりませんが、ここでは特に型名を指定していません。これがF#の特徴のひとつである型推論(type inference)という機能です。

今回は比較演算の評価結果がbool型であることから、resultbool型であると推論されました。このような単純な値の場合は型推論の恩恵はあまり感じられませんが、後に出てくる高階関数を定義する場合には大変便利です。

型推論に頼らずに、自分で型を指定することもできます。型を指定するには、識別子の右側にコロンにつづけて型名を書きます。このように型を指定することを型注釈(type annotation)といいます。では、実際に例をみてみましょう。

> let x : int = 3;; // xの型をintと指定
val x : int

> let result : bool = 3 > 1;; // resultの型をboolと指定
val result : bool

> let result : string = 3 > 1;;  // resultの型をboolと指定
  let result : string = 3 > 1;;
  ----------------------^^^^^^
stdin(7,23): error FS0001: This expression has type
  bool
but is here used with type
  string.

一番最後の入力では、指定した型と右辺の式の型が一致しないためにエラーとなっています。

ここまでは、intstringなどの値に対して識別子を束縛する方法について説明しましたが、F#では関数に対しても同じ方法で識別子を束縛します。以下に例を示します。

Note

関数に関する説明は、後の章の「関数の基本」できちんと行うので、ここでは関数について深く考えなくても大丈夫です。

> let plus x y = x + y;; // xとyを加算する関数に対してplusという名前を束縛
val plus : int -> int -> int

> plus 4 3;; // plusを呼び出す
val it : int = 7

ここでは、加算をする関数に対してplusという識別子を束縛しています。束縛する方法は、先ほどまでのintstringなどの値に対する方法とほとんど同じで、letを使います。ひとつ違うのは、識別子名(plus)の後にいくつか引数(xy)が並んでいることです。これらの引数は、関数の本体(x + y)で使用されています。この例を見てわかるように、F#では関数だろうと値だろうと、letによってほとんど同じように束縛されます。

3.2. 識別子名の命名規則

ある値に識別子を束縛すれば、あとからその値を参照することができます。適切に識別子をつけることは、プログラムの読みやすさや保守性の面でとてもよいことです。識別子の名前はUnicode文字で、先頭が数字でなければ好きな名前をつけることができます。たとえば、数式を表現するのに識別子名にギリシア文字を使うこともできます。

Note

Unicode文字なら名前に使うことができるので、もちろんひらがなや漢字も使えます。厳密にいうと識別子の名前に使えない文字も多く存在しますが、通常の文字はたいてい使えると思ってください。厳密な制限は言語仕様を参照してください。

ちなみに、この言語仕様の中では'\Lu''\Pc'などの表現が出てきますが、これはUnicode文字クラスというもので、文字をグループを表現します。たとえば、'\Pc'は「Punctuation, Connector (句読点、接続)」の文字グループです。詳しくはMSDNの文字クラスをご覧ください。

ただし、以下の名前はF#によって予約されているので使うことはできません。

abstract and as assert base begin class default delegate do done downcast downto elif else end exception extern false finally for fun function if in inherit inline interface internal lazy let match member module mutable namespace new null of not open or override private public rec return static struct then to true try type upcast use val void when while with yield

また以下の名前は、OCamlとの互換モードを使ってコンパイルする場合に使用するので、そのために予約されています。

asr land lor lsl lsr lxor mod sig

さらに、以下の名前は将来のために予約されています。

atomic break checked component const constraint constructor continue eager event external fixed functor global include method mixin object parallel process protected pure sealed tailcall trait virtual volatile

3.3. 練習問題

  1. 以下のlet束縛に適切な型注釈をつけてください。

    (例) let x = 3.14mlet x : decimal = 3.14m

    1. let x = 100

    2. let x = 3.14

    3. let x = [1; 2; 3]

    4. let x = [1y, 2y, 3y]

    5. let x = [(0,"hello"); (1,"world!")]

    6. let x = [(0,"hello"), (1,"world!")]

    7. let x = [[|0; 1; 2|]; [||]]

inserted by FC2 system