ブログ「サイバー少年」

ブログ「サイバー少年」へようこそ!
小学六年生ごろからプログラミングを趣味にしている高校生のブログです。
勉強したことについての記事などを書いています。フリーソフトも制作、公開しています。
(当ブログについて詳しくは「ブログ概要紹介」を参照)

サイバー少年が作ったフリーソフトは「サイバー少年の作品展示場」へ

F#の基礎勉強まとめ (後編)

記事「F#の基礎勉強まとめ (前編)」の続きです。
前編を読んでいない場合は、F#の知識がない限り前編から読むことをお奨めします。

今回は、かなり長くなることが予想されますね。


最初に、前編で書いたことの訂正というか、多相型についての発見があったので記しておきます。

まず、多相型の名前は'aのようにシングルクォーテーションとアルファベットだと説明しましたが、実際はシングルクォーテーションと任意の名前で多相型を表せるようです。
処理系が多相型であると推論したときは自動的にアルファベット一文字が使われるというだけのことで、自分で書く多相型の名前はなんでもありです。


そして、関数の引数が多相型になる場合があると説明しましたが、引数の型が多相型であると明示できるようです。
そのためには後述するパターンの型注釈を使用して、型名を多相型にします。

let func (x:'a) = x

しかし、多相型の引数であると明示したとしても、たとえば

let func2 (x:'a) = x + 1

と書いたらxはint型しかありえません。
このように型を限定するようなコードを書いたら多相型がもっと狭い型へと“制約”されてしまうので、なんでもかんでも多相型に出来るわけではありません。

さらに言ってしまえば変数でも型注釈で多相型に出来てしまうのですが、値を入れた段階で多相型がその型へ“制約”されてしまうので、多相型の変数というのも宣言できません。


また、同じスコープ内で複数回、同じ名前の多相型を使う場合、それら全てが同一の型であるという前提のもとで上手な型推論が行われます。

たとえば
let func3 (x:'a) = let val:'a = x in val
これはvalの型はxと同じで、xは多相型ですからxに与えた引数によってvalの型や戻り値の型が決まるようなジェネリックな関数です。

let func4 (x:'a) = let val:'a = 3 in val
これはvalの宣言によって'aがint型に“制約”されるので、この関数に与える引数はint型でなければなりません。


この話題は依然として謎深く、個人的にさらなる研究が必要そうです。





リストと配列

F#では配列と、連結リストも言語レベルでサポートしています。
なお、シーケンスという似たものもあるようなのですが、調べていないので扱いません。

まず、リストは[]を書いて、その中に要素をセミコロンで区切って書いていきます。
要素を書かず空のリストを記述することも可能です。

let l = ["January";"February";"March"]

要素の型はすべて同じでなければいけません。
そして、リスト全体の型は(要素の型) listという名前の複合型となります。
空リストの場合は要素の型が多相型とみなされるようです。


ひとつの要素を既存のリストの先頭に連結させたリストを返す演算子があります。
要素を書いて::を書いてリストを書きます。

let m = 0::[1;2;3]
let n = -2::-1::m // -2::(-1::m)とみなされる

また、リストとリストを連結したリストを返す演算子もあります。
前のリストと後ろのリストを@で繋いで書きます。

let o = m@[4;5;6]


続いて配列です。
配列は[]で囲んで、内側に||を書いて、その中に挟む形で要素を書いていき、セミコロンで区切ります。

リストもそうなんですが、最後にもセミコロンを書いて問題ないようです。
というかF#のセミコロンで区切る系の構文はすべてそうだと思います。

let a = [|1;0;0;1;1;0;0|]

こちらも要素の型は同じである必要があり、配列の全体の型は(要素の型) []という名前の複合型になります。

および多相型の配列として要素を空にした配列も記述できます。
F#に限った話ではないですが、空の配列ってメモリ配置的にどうなってるんでしょうね。


配列はリストのように連結することは出来ませんが、インデックスで要素にアクセスすることが出来ます。
インデックスは0から始まります。

let item = a.[2] // itemは0

リストを書いてドットを書いて、[]を書いて中にインデックスを書きます。


リストと配列の両方においてint型を要素とするときに、セミコロンで区切った要素を書く代わりに(最初の値)..(最後の値)と書くことで、最初の値から最後の値までの要素を列挙することが出来ます。

let l2 = [1..5] // l2は[1;2;3;4;5]
let a2 = [|-5..0|] // a2は[|-5;-4;-3;-2;-1;0|]

[5..1]みたいに逆向きになるのは無理で、この場合は要素が空となります。

また、(最初の値)..(増やす値)..(最後の値)と書くことで、最初の値から始まり、増やす値だけ加算されていって、数値が最後の値より大きく(増やす値が負数なら小さく)なるまでのものを要素とすることも出来ます。

let l3 = [1..2..10] // l3は[1;3;5;7;9]
let a3 = [|10..-2..-4|] // a3は[|10;8;6;4;2;0;-2;-4|]



タプル

F#にはタプルという2つ以上の要素の組を表すものがあります。
リストや配列のように要素の数が可変なのではなく、要素の数はタプルの型で決まります。

また、リストや配列では要素の型はすべて同じでなければなりませんが、タプルの場合はタプルの型によって何番目の要素は何型であるということが決定されます。

数学でいうところの順序対、データベースでいうところのレコードがタプルです。
そのため、データのコレクションをタプルで表すというのは適していません。


タプルを記述するには、全体を()で囲んで、それぞれの要素をコンマで区切ります。
()で囲まなくてもいいのですが、どこがタプルの要素なのか明示するために()で囲むほうがいいでしょう。

let t = ("January",1)

この場合のタプルの型はstring * intという名前の複合型となります。
一般にn個の要素を持つタプルの型は(1番目の要素の型) * (2番目の要素の型) * ... * (n番目の要素の型)と表されます。

関数の引数などがタプルのときに要素の型が多相型となることはありますが、基本的に任意のタプルを表す型は存在しません。


2つ組のタプルの1番目の要素を返すfstという演算子と、2番目の要素を返すsndという演算子があります。

let t1 = fst t // t1は"January"
let t2 = snd t // t2は1

要素の数が3以上のタプルで任意の番目の要素を取得したい場合は、後述するパターンというものを使います。




レコード

レコードというのもタプルと同じく要素の組を表すものですが、タプルがそれぞれの要素を何番目にあるかで識別するのに対して、レコードは要素に名前を付けます。
名前で要素を管理するために、要素にどんな名前を付けるかの定義、つまりレコード型の定義を最初にします。

レコード型の定義によって、レコードのそれぞれの要素の名前と要素の型を定義します。
レコードでは要素のことを、特に値の入れ物という意味合いでフィールドと呼びます。

type person = { name:string; age:int }

このようにレコード型の定義はtype (レコード型名) =と書いて、続いて{}の中に(フィールド名):(フィールド型)と書いたものをセミコロンで区切ってフィールドを定義していきます。

これでpersonという型が定義されました。
続いて実際のレコードの値を作成します。


let cb = { name="サイバー少年"; age=17 }

{}の中に、(上で定義したフィールド名)=(フィールド値)というのをセミコロンで区切って全て書いていきます。
順番は異なっていても構いません。


このときレコード型の名前を明示していませんが、フィールド名からレコード型を推論してくれます。

しかし、実験してみたところレコードの型推論はあまり賢くないようで、レコードの値として記述したフィールド名をすべて持つレコード型の中で、レコードの値を記述した場所より上で定義したレコード型の中で一番下のものであると推論されると思われます。

下のレコード型が上のレコード型にフィールド名で覆いかぶさっている場合、下のレコード型の値のみ作成できるわけです。
たとえば

type record1 = { field1:int; field2:int }
type record2 = { field1:string; field2:string }

let val1 = { field1=5; field2=6 }

これはval1がrecord2型と推論されるので、フィールドの型が異なってエラーです。

type record3 = { field1:int; }
type record4 = { field1:string; field2:string }

let val2 = { field1= 10 }

これはval2がrecord4型と推論されるので、field1の型が違うおよびfield2が足りなくてエラーです。


ただし上で定義したレコード型の値を作成したい場合は、

let val3 = { record1.field1=5; record1.field2=6 }

というふうに、フィールド名の前にレコード型名とピリオドを付ければレコード型を明示できます。
どれかひとつのフィールドでレコード型を明示すれば、そのレコード型として推論されるのでOKです。

もしくは、変数や関数の引数に入れる場合、名前は後述するパターンなので型注釈というものを付けられるのですが、それを使ってレコード型を明示することもできます。

let val4:record3 = { field1=10 }


最後に、レコードのフィールドの値を取得するには、

let val5 = { field1="Hello"; field2="World" }
let f1 = val5.field1
let f2 = val5.field2

のように、レコードの値を書いて、ピリオドを書いてフィールド名を書きます。


なお、フィールドの型を自分自身のレコード型にすることも出来ます。
ただし、そうしたところでレコードの値の記述は、無限入れ子構造となって不可能ですので、後述するoption型を使うなどして工夫が必要です。



判別共用体

F#では判別共用体という、いくつかの値からどれかひとつを持ちうる型を定義することが出来ます。
いくつかの値というのは好きな値を持たせていいわけではなくて、名前によって識別される判別共用体のための値だけです。

C#などでいうところの列挙型にあたります。

判別共用体を定義するには、typeを書いて型名を書いて=を書き、|(値の名前)というのを持ちうる値のぶんだけ繰り返し書いていきます。
ただし値の名前は、最初の文字は必ず大文字でなければなりません。

type day =
|Sunday
|Monday
|Tuesday

これで3つの値を持ちうるday型が定義されました。
day型の値を記述するには、値の名前を書くだけです。

let today = Monday

なお値の記述では、別々の判別共用体で同じ名前の値が定義されていたら、値を記述した場所より上で定義した判別共用体の中で、記述した名前の値を持つ判別共用体の中で一番下のものであると推論されます。

それより上で定義した判別共用体の値を記述したい場合は、レコードのときのように型名を書いてピリオドを書き、値の名前を書くことで型を明示できます。

let today2 = day.Monday


さて、実は判別共用体はここまで説明した機能だけでなく、列挙体だけではとどまらない機能を持ちます。
それはそれぞれの値に任意の型の付加情報を持たせられるというものです。

どのような型の付加情報を持たせるかは、判別共用体の値によって決まります。
付加情報を持たせる場合は、値の名前を書いたあとにofと書いて型名を書くことで付加情報の型を表します。

type varioustypes =
|Integer of int
|Boolean of bool
|String of string

このように、付加情報の型は値によって異なるものを設定できるわけです。
つまり、varioustypesというひとつの型にint型やbool型などの異なる値が混在しうるという点で、C言語などの共用体とも似ています。

さらに値の名前によって何型の付加情報が含まれているか“判別”できるので、付加情報メインの視点において判別共用体という名前であるのだ、と思います。


さらに、ひとつの値に付加情報を複数持たせることが可能です。
その場合は、ofのあとの複数ある付加情報の型名を*で区切ります。

type variouspair =
|BooleanAndInteger of bool * int
|StringAndUnit of string * unit

これはひとつのタプルの値を設定することで複数の値を設定しているのかと思ってしまいますが、実はひとつのタプルではなく複数の値があるものとして設定されます。

それは、後述するパターンで
let BooleanAndInteger(a,b) = BooleanAndInteger(false,6)
と書いてa,bに値を入れるのは出来るが
let BooleanAndInteger t = BooleanAndInteger(false,6)
というふうにタプルとして値を入れるのはエラーになることからもわかります。
(後述のパターンについて読んでから読みかえしてください。)


付加情報を持つ判別共用体の値の記述は、値の名前を書けば、それは付加情報を引数にとって判別共用体の値を返す関数とみなされるので、そこに付加情報を与えます。

付加情報まで書いてはじめて判別共用体の値となります。
そういう意味では付加情報というより、値の構成要素というほうが適切です。

let val = BooleanAndInteger(true,10)

なお、付加情報が複数ある場合はタプルではないと言いましたが、この関数の引数はn番目の付加情報をn番目の要素に対応させるタプルです。
この関数はカリー化されていないので、他に複数の値を渡す手段はタプルしかないからでしょう。


付加情報の型を自分自身の判別共用体にすることも出来ます。

type mylist =
|LastNode of int
|Node of int * mylist

このコードで連結リストを定義できました。
結構、複雑なデータ構造を表現できますね。

当然のことながら、判別共用体のすべての値が自分自身の型を付加情報に持つ場合は、無限入れ子構造になってしまい値の記述が出来ません。


判別共用体は普通、後述するパターンマッチ、match式と組み合わせて値の名前によって柔軟に付加情報を取得することになります。



ジェネリック型とoption型

typeキーワードで定義するレコードや判別共用体ではジェネリック型、つまり型を定義する段階では要素に使う特定の型を設定せずに、型を使う段階で具体的な型を与えるという機能があります。

実は関数定義でもジェネリック型を使えるのですが、調査不足なので本記事の冒頭で軽く触れただけにとどめます。

ジェネリック型を使うには
type 'a generictype1 = { field:'a }
のように、typeと型名の間に多相型の名前を書くか、
type generictype2<'a> = { field:'a }
のように、型名を書いたあとに多相型の名前を<>で囲んだものを書きます。

ここで書いた多相型の名前を、レコード型などの型を使うほうから具体的なジェネリック型をもらう入れ物という意味で型引数と呼びます。
型引数は型名の代わりとして、レコード型などの型の定義の中で好きなように用いることができます。

2通りのジェネリック型の記述方法を示していますが、型引数が2つ以上ある場合は後者の方法のみ可能で、複数ある型引数をコンマで区切って<>の中に書きます。

type generictype3<'a,'b> = { filedA:'a; fieldB:'b }


このようなジェネリック型を含むレコード型などの型の型名を記述する場合は、こちらも型引数が1つだけなら型引数に与える型名を書いてレコード型などの型の型名を書いて、
もしくは一般にはレコード型などの型の型名を書いたあと<>を書いて中にコンマで区切った与える型名を書きます。

および、このようなレコード型などの型の値を記述する場合は、ジェネリック型に対しても型名を与えずとも型推論が働きます。

let val1 = { field="test" } // val1はstring generictype2型
let val2 = { fieldA=5; fieldB=10 } // val2はgenerictype3<int,int>型


ジェネリック型を使用する判別共用体のoptionという型がF#では標準で定義されています。
実は単純な判別共用体ではないのでもう少し複雑らしいのですが、定義の概要は以下です。

type 'a option =
|Some of 'a
|None

これはC#などで使うNull許容型のようなものです。

値が入っているかどうかわからないときに、値があるならSomeという名前と実際の値が入って、値がないならNoneという名前だけが入るもので、普通は後述するパターンマッチと合わせて使用します。

let val3 = Some 9 // val3はint option型
let val4 = None // val4は'a option型

このとおりNoneの型を指定しなければ多相型を型引数に与えるoption型と推論されます。




パターン

F#には様々な値を統一的に表現するための、パターンというものがあります。
特定の値が特定のパターンに属するか、すなわち“マッチ”するかどうか調べることが可能で、パターンマッチといいます。

パターンマッチとはどんなものか確かめるには後述のmatch式を使う方法もありますが、簡単なのはletキーワードを使う方法です。
前編で書いたとおりletのあとの変数名は実はパターンで、続く=のあとにくる値をマッチさせようと試みています。

たとえば、1や"hello"などのリテラルを書いたものもパターンとして機能して、これはそのリテラルの値のみにマッチするパターンです。

let 1 = 1
これはマッチに成功します。
let 1 = 2
これはマッチに失敗し、後述する例外というものを発生させます。


ちなみに、研究不足なので断言は出来ませんが、値に型があるようにパターンにも型があるようで、パターンが異なる型の値を横断して表現しうることはありません。

let 3 = "hello"

のように値とパターンが異なる型でパターンマッチさせようとすると、明らかにマッチしないので文法エラーになります。
値とパターンが同じ型であるがマッチしなかった場合は前述のとおり文法エラーでなく例外を出します。


パターンは様々な値を表現するために、いくつかの種類があります。

MSDNを見てみるとたくさん種類があるので、すべて紹介するというわけにはいきませんが、ここまで解説してきたデータ構造を表現するためのパターンなどを紹介します。


リテラルパターン

let "hello" = "hello"

先ほど使った、プリミティブ型のリテラルを書いて、書いてあるリテラルの値だけにマッチするパターンです。


ワイルドカードパターン

let _ = "hello"

_を書くことで、あらゆる値にマッチします。
先ほど述べたパターンに型があるという仮説が正しいなら、ワイルドカードパターンは多相型であると思います。


変数パターン

let a = 5

今まで行ってきた変数宣言は、この変数パターンを利用しています。
これはaやvalなどと、なにか名前を書くのですが、あらゆる値にマッチします。

そしてパターンマッチ以降のコードにおいて、その名前を使って、マッチした値を参照することが出来ます。
その名前のスコープはパターンマッチをする構文によります。

後述のORパターンでそれぞれの中のパターンに同じ変数パターンを用いる場合を除いて、ひとつのパターンの中に同じ名前の変数パターンを複数、用いることは出来ません。

また、先ほど述べたパターンに型があるという仮説が正しいなら、変数パターンは多相型であると思います。
しかし値を入れることで値の型へと“制約”されます。


ORパターン

let 1|2|3 = 2

いくつかのパターンを|で区切って書くことで、中のパターンで再帰的にパターンマッチを行ってそのなかのいずれかとマッチする値にマッチします。

なお、中のパターンで変数パターンを用いる場合、すべての中のパターンで同じ名前の変数パターンらが使われていて、それらはすべての中のパターンにおいて、それぞれ同じ型でなければなりません。

中のどのパターンにマッチしても、変数パターンには値が入らないといけないからです。
この場合は中のパターンを左から、値とパターンマッチさせていって、一番最初にマッチしたところの変数パターンで割り当てられます。


ANDパターン

let 3&a = 3

いくつかのパターンを&で区切って書いて、中のパターンで再帰的にパターンマッチを行い、すべてのパターンにマッチする値にマッチします。
上の例では値が3にマッチするかどうか調べると同時に、値をaという名前の変数にいれています。

それを実現するにはasパターンというものもあって用途がかぶるのですが、ANDパターンのほうが柔軟です。
もちろん、わかりやすさのためにasパターンを使うこともあるでしょう。


括弧

let (1|2|3)&a = 2

中に入れ子になってパターンを書くパターンの場合、どこからどこまでが入れ子になっているのか、つまり、どのようにパターンが結合されているのかが曖昧で、誤って解釈されてしまうことがあります。

その場合は括弧で囲んで括弧内のパターンを優先的に結合させられます。


型注釈

let a:int =  4

パターンを書いたあと:(型名)と書きます。
これは中のパターンにマッチして、さらに型が隣で指定したものである値にマッチします。

先ほど述べたパターンに型があるという仮説が正しいなら、これは中のパターンの型を指定した型で上書きすると思います。

もっとも
let 5:string = 5
などと書いたらパターンが何型であるのか、よくわかりませんが…。
(これは文法エラーです。string型の値を入れても文法エラーです。)

関数の引数もパターンですので、引数に型注釈を設ければ引数の型を制限できます。


タプルパターン

let (a,b,c) = (4,1,"test")

タプルを記述するように書くパターンで、要素を書くところにはそれぞれパターンを書きます。
中のそれぞれのパターンと対応する要素でパターンマッチを行って、すべてマッチするようなタプルにマッチします。

前述したとおり、タプルパターンと変数パターンで要素が3つ以上のタプルから任意の番目の要素を取得することが出来ます。

また、関数の引数をタプルパターンにすることで、カリー化でない複数の引数を持つ関数を定義できます。

let func (x,y,z) = x + y + z


リストパターン、配列パターン

let [a;b;c] = [1;10;100]
let [|x;y;z|] = [0;1;2]

リストや配列を記述するようにして書くパターンで、要素を書くところにそれぞれパターンを書きます。
リストやタプルの解説で紹介した[1..10]のような記法はパターンでは使用できず、ひとつずつ中に要素のパターンを書いていきます。

中のパターンの数が要素数と同じで、中のパターンとそれぞれ対応する要素でパターンマッチを行って、すべてマッチするようなリストや配列にマッチします。


Consパターン

let h::tail = ["first";"second";"third"]

リストの要素を表すパターンを左に書いて、::を書き、リストを表すパターンを右に書くパターンです。
空でないリストに対して、リストの先頭の要素と左のパターンでパターンマッチ、およびリストの後続の部分リストと右のパターンでパターンマッチを行い、両方でマッチするようなリストにマッチします。

要素が1つだけのリストでも先頭の要素と後続する空リストに分割できることに注意です。なお

let f::s::t::[] = ["first";"second";"third"]

のように入れ子にして書けるわけですから、空のリストパターンとConsパターンで任意のリストパターンの代用になります。


識別子パターン

let Some x = Some "value"

これは簡単にいえば判別共用体の値を表すパターンです。
上で定義されている判別共用体の値を書くようにして、引数のところにパターンを書きます。

前述のとおり、引数がタプルである場合は引数のところのパターンは、ひとつの変数パターンを書いたりは出来ず、タプルパターンを書かなければなりません。

同じ名前の値であり、引数のところに書いてあるパターンと付加情報でパターンマッチを行って、マッチするような判別共用体の値にマッチします。
付加情報が複数ある場合は、引数のところに書いてあるタプルパターンのそれぞれの要素のパターンに、対応する付加情報をパターンマッチさせて、すべてマッチするかどうかを調べます。

識別子パターンの型は、書いてある判別共用体の値の型として推論されると思います。

判別共用体は識別子パターンでパターンマッチさせて、付加情報を変数パターンで取得するというのが普通の使い方なわけです。
また、後述する例外は判別共用体なので、識別子パターンは例外処理にもよく使われます。


レコードパターン

type myrec = { fieldA:int; fieldB:string }
let { fieldA=a; fieldB=b } = { fieldA=8; fieldB="value" }

レコードの値を表すパターンです。
上で定義したレコードの値を書くようにして、フィールドに入れる値を書くところにパターンを書きます。

これは、中のそれぞれのパターンをレコードの値の対応するフィールドの値とパターンマッチさせて、すべてマッチするようなレコードの値にマッチします。

実は、レコードパターンはレコードの値を作成するわけではありませんので、

let { fieldB=x } = { fieldA=8; fieldB="value" }

のように、すべてのフィールド名と値のパターンの組を書かず、一部だけを書くことも出来ます。
その場合は書いてあるフィールドのみ、中のパターンとフィールドの値がパターンマッチされ、それらがすべてマッチするならマッチします。

パターンに型があるという仮説が正しいなら、フィールドをすべて書くにせよ書かないにせよ、レコードパターンの型は明示しない限り、パターンに書いてあるフィールドをすべて持つ上で定義したレコードの中で、一番下のレコードの型として推論されると思います。

つまり、前述した普通にレコードの値を記述するときのレコードの型の推論と同じです。



match式

F#のmatch式はC#などでいうところのswitch文に相当しますが、switch文がリテラルによる条件分岐しか出来ないのに対して、match式は前述のパターンにマッチするかどうかで条件分岐をさせられます。

つまり、パターンと式の組をいくつか書いていき、上から順番にパターンマッチを試みて、入力の式が最初にマッチしたパターンのところの式が戻り値となります。

戻り値の式は型を揃えなければなりません。
また、パターンに型があるという仮説に基づきますが、前述のとおりパターンマッチですので入力の式と下のパターンは型を揃えなければなりません。

let a = Some 3 in
match a with
|None -> printfn "a is no value"
|Some x -> printfn "a is %d" x

このように、matchと書いて入力の式を書いてwithと書き、|(パターン) -> (戻り値の式)というのを条件分岐のぶんだけ列挙していきます。
変数パターンにマッチした値は、そこの戻り値の式において参照が可能です。

繰り返し言いますが、入力の式が最初にマッチしたパターンのところの式が戻り値となります。
戻り値とならなかった式は評価されません。

入力の式がいずれのパターンにもマッチしなければ、後述する例外を発生させます。
ですので、その例外をキャッチするか、もしくは一番最後のパターンにワイルドカードパターンなどを置いて、上のパターンに一致しなかった場合の値を記述します。


また、match式では、パターンを書いたあとwhenを書いて任意の条件式を書くことで、パターンにマッチして、なおかつ条件式を満たすものを戻り値とさせることも出来ます。

もちろん、変数パターンでマッチした値を条件式の中で参照することが出来て、普通はその値についての条件式を書きます。

let b = Some 15 in
let result = match b with
|None -> "b is no value"
|Some x when x > 10 -> "b is a value greater than 10"


パターンにORパターンを使っている場合、その中に変数パターンを使っていたらORパターンの中の一番最初にマッチしたところの変数パターンで割り当てられると前述しましたが、

もしかしたら下記のように、一番最初にマッチしたところの変数パターンで割り当てたらwhenの条件式を満たさなくても、二番目以降にマッチするところの変数パターンで割り当てたら条件式を満たすかもしれないわけです。

let t = (0,3)
match t with
|(x,y)|(y,x) when x=3 -> printfn "x is 3"

whenを使うとそのような可能性も考慮して、ORパターンの中のマッチしうるパターンを一番最初から順番に調べていって、
それぞれのパターンの変数パターンの割り当てでwhenの条件式を満たすかどうか調べるというのを、条件式を満たすまで行ってくれます。

結果的に変数パターンの割り当ては、一番最初に条件式を満たした割り当てとなります。


また、functionというキーワードを使って、ひとつの引数を持ち、それをmatch式でパターンマッチさせるような関数を記述できます。
つまりmatch式の糖衣構文です。

let func = function
|"January" -> 1
|"February -> 2
|"March" -> 3

このように、functionと書いて以降はmatch式のパターンと戻り値の式の組を書いていくことで、引数をmatch式でパターンマッチさせてmatch式の戻り値の式を関数の戻り値とするような関数を記述します。



例外

例外というのはC#など多くの言語に存在する機能ですが、プログラムの実行中にエラーが発生して処理を継続できないときに、処理を中断して例外というものをスローさせられます。

スローというのは例外を発生させて、発生した関数からその関数を呼び出した関数へ、さらにその関数を呼び出した関数へと例外を移動させるということで、例外をキャッチする、つまり例外に対してエラー処理を行って例外のスローを止めるまで続きます。

例外はひとつではなく種類があって、C#などではクラス型によって例外の種類を定義しますが、F#では判別共用体の値として例外の種類を定義します。
しかし、F#での文法上は判別共用体の値となるのですが、C#などとの親和性のため内部的にはクラス型としても定義されるそうです。


F#で例外を使うには、まず例外の種類を定義する、次に例外をスローする、最後に例外をキャッチする、の3段階となります。
例外の種類の定義と例外のスローは勝手に行われているものもありますが、自分でするにはどうするか説明します。

例外の種類の定義ですが、前述したように判別共用体の値として定義します。
ただし、type (型名) =と書いて|を書いて値の名前を書いていくのではなく、いきなりexceptionと書いて、値の名前を書いて必要なら付加情報を書きます。

exception MyError of string

付加情報にはエラーメッセージや詳細情報などを付けます。
この判別共用体の値はexnという名前の判別共用体に属しているようですが、それぞれの値は特別にSystem.Exception型としても振る舞います。


続いてこれをスローするのでしたが、例外を引数に取ってその例外をスローするraiseという関数がありますので、これを使います。

raise(MyError "This is an error.")

この関数の戻り値の型は、どのみち例外が発生して戻り値は返ってこないので、どんな型であってもいいんですが、

let devide x y =
if y=0 then raise(MyError "Cannot devide by zero.")
else x / y

みたく直接、if式などの戻り値として書けるように、つまり例外が発生しなかった場合の戻り値の型に合わせられるように、多相型になっています。


最後にスローされた例外のキャッチですが、try-catch式というのを使います。

まずtryと書いて例外をスローする可能性のある式を書きます。
続いてwithと書いて、あとはmatch式と同じようなパターンと戻り値の式の組を書いていきます。

try
printfn "throw an exception soon";
raise(MyError "error has thrown")
with
|MyError _ -> printfn "catched MyError"

そういえば式を書くところで複数の式を書くのは初めてですが、セミコロンで式を区切ることで順番に式を評価していって、最後の式が戻り値となる複合されたひとつの式を書くことができます。

try-catch式は通常はtryとwithの間の式を評価して戻り値とするのですが、その式で例外がスローされると式の評価を中断し、スローされた例外を判別共用体の値としてキャッチして、withの下のmatch式でパターンマッチします。

そしてマッチしたパターンのところの戻り値の式が評価されtry-catch式の戻り値となります。
もちろん、戻り値の式は型を揃えなければなりませんし、例外がスローされない場合はtryとwithの間の式が戻り値となるわけですので、その型とも同じでなければなりません。

マッチするパターンがなかった場合はもう一度、その例外をスローします。
これは自分で明示的に行うことも可能です。

そのためにはワイルドカードパターンなどを最後に置いて、reraiseという関数が戻り値の式のところで使えるのですが、これはunit型の値を引数に取り、直属の戻り値の式のキャッチした例外をもう一度スローする関数ですので、これを使います。

私が勉強で使ったサイトではrethrowという名前だったのですが、reraiseに変わったようです。

try
raise (MyError "#1")
with
|MyError "#1" -> printfn "catched MyError #1"
|_ -> reraise()

reraise関数を変数に入れておいて、直属の戻り値の式が変わったところで変数に入れておいたreraise関数を呼び出したら、
直属の戻り値の式より前でキャッチした例外をスローするか…などと考えましたが、そもそもreraise関数は特殊で変数に入れられないようです。

クロージャの話です。
私はこのような関数型言語のシステムについて詳しくないので、詳しい言及は避けます。


開いたファイルを閉じるなど、例外が発生しても発生しなくても確実に終了のための処理を行わせたい場合は、try-finally式というのを使うことが出来ます。
tryと書いて戻り値の式を書いて、finallyと書いて終了のための処理を行う式を書きます。

let fs = System.IO.File.Create("test.txt") in
try
fs.WriteByte(65uy) // byte型の数値を表すにはuyを付ける
finally
fs.Dispose()

tryとfinallyの間の式が戻り値となりますが、その式で例外がスローされても、されなくても、tryとfinallyの間の式の評価から抜けるときに、終了のための処理を行う式を評価します。
tryとfinallyの間にtry-finally式をネストしている場合は、例外がスローされたら内側の式から終了のための処理を行う式が評価されていきます。

例外のキャッチは行いません。
そのためtry-catch式と組み合わせてよく使います。


上記コードのようにDisposeメソッドを持つオブジェクトの破棄にtry-finally式を使うなら、useというキーワードを使った糖衣構文があります。
これは、変数宣言の代わりにuseキーワードを使って、inと書いてtry-finally式で戻り値となる式を書きます。

use fs = System.IO.File.Create("test2.txt") in
fs.WriteByte(65uy)

useキーワードで宣言した変数がnullでないならDisposeメソッドを呼び出すというfinallyの下に書く式が内部的に付けられます。
終了のための処理が単純にひとつのオブジェクトのDisposeメソッドを呼び出すだけなら、useキーワードを使うほうが簡潔です。


最後に、F#の標準で定義されている例外の種類や、特定の種類の例外をスローする関数をひとつ紹介します。

Failureという名前の例外、正式名称はFailureExceptionで、一般的な例外を表すそうです。
failwith関数はstringの引数を1つ持ち、それを付加したFailureをスローする関数です。




やはりだいぶ長くなりましたね。
勉強した内容を前編と後編でほぼすべて書きましたが、モジュールと名前空間、参照型については時間や労力の都合上、扱いませんでした。


ですが簡単に説明しておくと、まず参照型というのは変数に値を入れるときにref (値)と書くことで、値が直接、入るのではなく値の参照が変数に入ります。
変数の内容は不変なので変更できませんが、参照先の値は特別に変更できるので、それを使って擬似的に値の変更が可能な変数を宣言できるという機能です。

モジュールはコード(定義の集合)をmodule (モジュール名) =と書いたあとbeginとendというキーワードで囲むことで、そのコードにある定義がこのモジュールに属します。
モジュールの外からモジュールの中の定義を参照するにはモジュール名を書いてピリオドを書いてから定義の名前を書かなければならないので、同じ名前の定義の衝突を避けられます。

名前空間はモジュールと同じようなものですが、こちらはC#などでの名前空間に相当するものなのだと私は推測します。
ですので、letによる変数定義などは名前空間に直に配置できません。
その視点においては、モジュールはVB.NETのモジュールに相当するようなものだと思うので、名前空間より下のレベルではないでしょうか。


というわけで以上ですね。
前編は執筆に3日かかったと言いましたが、今回、後編は前編とは比べ物にならないほど長い期間をかけました。

記憶では前編を公開してから1週間ぐらい経って後編の執筆を始めたんですが、まあときどき数日間の休みを挟みましたけど、わりと毎日数時間かけて執筆していました。
それで1ヶ月ぐらい経ってますから、どんだけ時間かけてんだって話ですね。

かなり内容の正確性にも気を使って、ゆっくり慎重に執筆した力作です。
ただ、私のブログは力を入れて書いた記事がたいして読まれずに、テキトーに書いた記事の人気が出ることに定評がありますから、なんだかなぁという感じですね。


まあ、それはいいとして、反省しなければならないのは説明がわかりづらいということでしょうね。
どうしても数学にハマっていることもあって、わかりやすさより正確性を重視して長文になってしまったり、サンプルコードを読めばわかるみたいな立場を取らず、言葉による説明をしようとしてみたり…ですね。

プログラミング言語なんてサンプルコード読んで勉強するものですけどね。

ただまあ、裏を返せば他のサイトがやっていないアプローチでF#の解説を試みているということですので、読んでいただいて「いいじゃん」と思っていただければ誠に光栄でございます。

しかしなにより、まず私がF#のことを詳しく知っているわけではないので、今後は実際にプログラムを書くことを中心に行っていって、F#の完全な習得を目指したいと思います。

F#の勉強に使っているサイトも全部読んだわけではないですしね。
F#にはオブジェクト指向プログラミングの機能もあるようなんですが、その説明を読んでないです。

なんて言ってたらまた長くなってしまいましたね。
というわけで、F#の基礎勉強まとめを前編と後編に分けてお送りしました。

tag: プログラミング F# 関数型 勉強まとめ 例外 リスト ジェネリックス パターン

コメント

コメントの投稿

トラックバック

トラックバック URL
http://cyberboy6.blog.fc2.com/tb.php/491-e99a5ec4
この記事にトラックバックする(FC2ブログユーザー)

当ブログをご利用(閲覧等)になる場合は必ず「当ブログの利用規定」をお守りください。