ブログ「サイバー少年」

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

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

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

ついにやってまいりました。
今までF#の勉強をしてきたわけですが、勉強に使っているサイトで基礎編とされている部分を読んでいまして、とうとう読み終わったので内容をご紹介したいと思います。

学んだ文法を使って実際にプログラムを書けるかというと、まだちゃんとしたプログラムを書いたことはないし、正直、書ける気がしないんですけどね…。
記事「2017年 新年のご感想」でも書きましたが、文法を学ぶことと実際にプログラムを書くスキルを身につけることは別物なんだと思います。

それも含めて自然言語と人工言語という違いはあれど、新しいプログラミング言語を学ぶことは英語などの外国語を学ぶことと似ていると、今回F#の勉強をして思いましたね。

新しい言語機能を覚えれば新しい表現が可能なこと、それは他の言語機能で代用しても近い表現をすることは出来るのですが、新しい言語機能を使うほうが便利です。


というわけで、実践的なプログラムの解説をすることはスキル上の問題で出来ませんが、文法の解説をしていきます。

なお、めちゃくちゃ長くなることが予想されたので前編と後編に分けることにしました。





変数

F#では変数宣言にletというキーワードを使って、

let a = 5

などというように書きます。

このaは変数名で、5は変数値です。
実は変数名は単純な識別子なのではなく次回に紹介するパターンというものになっています。

注意しなければならないのは、F#の変数は不変であるということです。
つまり変数宣言時のみに値をセットできて、あとからa = 10のように値を変更することが出来ません。

そのため、F#の変数はいわゆる箱をイメージするのではなく、特定の値に名前をつける機能と思ったほうがいいでしょう。


もちろん変数値は式でもいいですし、上で宣言した変数を値として参照することが可能ですので、

let b = a + 5

みたいに新たなbという変数を宣言することも出来ます。



型とリテラル

F#で扱う変数や値には型が存在します。
標準的な型は.NET Frameworkで使うプリミティブ型を使っていますので、intやfloat、string型、bool型などがあります。

floatについてはC#ではfloatが32bit浮動小数点数でdoubleが64bitなのに対して、F#ではfloatが64bitであって32bitの浮動小数点数はsingleという型名なので注意が必要です。


リテラルの表記ですが、普通に数字を打ち込めばint型の数値となります。
float型の数値を打ち込みたい場合は、整数部のあとに.0を打ち込んで小数とします。
もしくは.を打つだけでもOKでこれは.0と解釈されます。

let a = 5 // int型
let b = 5.0 // float型
let c = 5. // float型


任意の文字列をダブルクオーテーションで囲むことでstring型のリテラルとなります。
bool型のリテラルはtrueまたはfalseです。

let d = "Hello" // string型
let e = true // bool型

これらのプリミティブ型から複合型を作成することが可能ですので順次紹介します。


ちょっと特殊な型としてunit型というのがあって、これは()という何もないものを括弧で囲んだ値しか存在しない型です。

実質的に値が無いことを表すための型となります。
C#などのvoid型と似ていますがvoid型が実際に値が無い(未検証)のに対し、unit型には()という値があります。


また、多相型といってa'やb'などというようにアルファベットにクオーテーションをつけて表される型が登場することがあります。
これはちゃんと説明できる自信がないですが、登場している時点で型が決定されていないことを便宜上、表すための型であるといえます。

たいてい関数の型に登場して、戻り値が多相型になっているなら引数も多相型になっている、つまり引数の型によって多相型に具体的な型を与えるという仕組みになっています。
C#でいえばジェネリック型ですが、それとは違う点として型を直接与えるのではなく与えた引数の型から判別するわけです。



演算子

特定の型による1つの値や2つの値から新たな値を作り出す、演算子がもちろん存在します。

まずは+という加算の演算子ですが、オーバーロード、つまりintとintの値からintの値を作る+演算子やfloatとfloatの値からfloatの値を作る+演算子、stringとstringの値からstringの値を作る+演算子などが多重定義されています。

どの定義の演算子を使うのかはオペランド(演算する値)の型から判別されます。
+演算子の場合はintとfloatを演算するみたいな異なる型の演算は定義されていませんので、型を揃えなければなりません。

let a = 1 + 2 // aはint型の3
let b = 1.0 + 2.0 // bはfloat型の3.0
let c = 1 + 2.0 // エラー


続いて比較演算子を紹介します。
これらも必要に応じてオーバーロードされています。

等しいことを表す=演算子
let d = 2=2 // dはbool型のtrue(変数宣言の=記号と比較演算子としての=記号の区別に注意)
大小関係を表す>演算子、<演算子
let e = 3>2 // eはbool型のtrue
let f = 3<2 // fはbool型のfalse


bool型を使った論理演算子もあります。

論理否定のnot演算子
let g = not true // gはbool型のfalse
論理積の&&演算子
let h = g && true // hはfalse && trueなのでfalse
論理和の||演算子
let i = g || true // iはfalse || trueなのでtrue


なお、これらの演算子を、演算子の記号単体で括弧で囲めば、オペランドを引数に取る関数を作成できます。

let add = (+) // int -> int -> int型の関数



関数(1)

F#では関数は値です。

そのためF#の関数定義では、値としての関数を作成して変数にセットするというのが基本となります。
この値としての関数はfunというキーワードを使って作成します。

let inc = fun a -> a + 1

このようにfunのあとに引数名を書いて->を書いて、関数の本体となる式を書きます。
この引数名もletのときと同じく、実は正体は次回に紹介するパターンです。

関数呼び出しは、以下のようになります。
関数名のあとにスペースをつけて引数を書くか、関数名のあとに括弧をつけて引数を書きます。

let a = inc 5 // aは6
let b = inc(6) // aは7

C#とは違い、関数本体は式ですので戻り値のない関数を作成することは出来ません。

また、引数のない関数を作成することも不可です。
ただし引数はパターンですので、リテラルを引数のところに書いてもいいわけで、とくにunit型の値()を引数のところに書けば、引数がないように見える関数を作成できます。
つまり関数呼出しする側では関数名を書いたあと()を書くことになります。


関数は値ですので、型があります。
関数の型は(引数の型) -> (戻り値の型)という複合型で表されます。

上の関数では引数aの型が不明ですが、関数本体でa + 1をしていることから、intの値と一緒に+演算にかけられるのはintの値しかないので、aはint型であると処理系に推論されます。

よって上の関数の型はint -> intです。


let id = fun a -> a

このように引数の型を推論しようがない関数では、引数の型は多相型a'になります。
戻り値もこの場合は引数そのままですので、戻り値の型も同じa'型です。


関数の引数が複数ある場合は、それぞれの引数名(正確にはパターン)をスペースで区切ります。

let add = fun a b -> a + b

この関数の型はどうなるでしょうか。
これは実はint -> (int -> int)という型になります。

つまり、int型の引数を1つ与えるとint -> int型の関数が返ってくることになります。
その返ってきた関数にさらにint型の引数を1つ与えると、欲しかった戻り値であるint型の値が返ってきます。

このため関数呼び出しにおいては下記1行目のようになりますが、厳密に括弧をつけると下記2行目のようになります。

add 3 2
(add 3) 2


このことは、F#の関数が実は1つの引数しか持つことがなく、複数の引数を持たせたい場合は、内部的にはいくつかの関数の組み合わせで実現されるということを意味します。

たとえば上のadd関数は実は下の関数と同等です。

let add = fun a -> fun b -> a + b

複数の引数を持つ関数をこのような単一の引数を持つ関数の組み合わせに変換することをカリー化といいます。
引数の数が増えていくほど、実際の関数は階層が深くなっていきます。

引数を複数持たせたい場合は、このようにカリー化する方法が普通のようですが、次回に紹介するタプルを引数に持たせる方法もあります。



関数(2)

let add = fun a b -> a + b
みたいに定義してきた関数を

let add a b = a + b
のように省略して書くことができます。

つまりは、letを書いて関数名を書いて引数名(パターン)を書き、=に続けて関数本体となる式を書くことでも関数定義が可能です。

普通はこの省略を使用します。
なお詳細は知りませんが、厳密には単純な省略ではなく、funを使って関数定義する場合と別物になるそうです。


再帰関数を定義したい場合、関数本体の式の中に自身の再帰関数呼び出しを書くことになりますが、実は再帰関数呼び出しを書いた段階ではその関数の定義が完了していないので、呼び出しできずエラーになります。

しかし、letの後にrecというキーワードを付けることで、定義が完了していない関数を呼び出すことが出来るようになります。

let rec fact n =
  if n=1 then 1
  else n * fact(n-1)

私が勉強に使ったサイトの受け売りですが、上記はnの階乗を求めるコードです。
このコードで使っているif式については後述します。


いくつかの関数で循環している形式の再帰関数の場合は、最初の関数定義にlet recを用いて、以降の関数定義ではandキーワードを書いて関数名と続きます。
これにより、まだ定義していない関数を呼び出せます。

let rec even n =
  if n=0 then true else odd(n-1)
and odd n =
  if n=0 then false else even(n-1)

これはnが偶数であるかどうか判定するeven関数と、nが奇数であるかどうか判定するodd関数です。
これも勉強に使ったサイトの受け売りなのですが、再帰関数は私にとってハイレベルなので自分で例を考えられないためです…。

このコードはnがevenかどうかはn-1がoddかどうかということだし、nがoddかどうかはn-1がevenかどうかということ、そしてn=0のときevenはtrueでoddはfalseであるということを言っています。
2で割った余りで判定すればいいじゃんという話ではあるのですが、再帰的でテクニカルな表現をしてると思います。


再帰関数は再帰するほどにスタック領域を消費しますが、F#の場合だと末尾再帰関数、つまり再帰呼び出しによる戻り値に手を加えず、そのままそれを返す形の再帰関数をループに変換してくれます。

よってこの形では再帰するほどにスタックを消費しませんので、なるべく末尾再帰関数で書けるところはそうするべきでしょう。



if式

F#ではif文ではなくif式というものがあります。

let a = 5
let result = if a=5 then "a is 5" else "a isn't 5"

このようにifの後に条件式を書いて、thenを書いてひとつめの式を書きます。
そしてelseを書いてふたつめの式を書きます。

条件式がtrueと評価された場合は前者の式をif式の戻り値として、falseと評価された場合は後者の式をif式の戻り値とします。
戻り値にならなかったほうの式は評価しません。

上記コードの場合、resultは"a is 5"となります。


if式全体の型を決定するために、戻り値の式は型を揃えないといけません。
戻り値の型がunit型の場合、else以降を書かないことも出来ます。

戻り値がunit型だということは、たいていが副作用のある関数を呼び出しているでしょう。
下記コードは値を出力する基本的な関数で、戻り値がunit型のprintfnを呼び出しています。

let b = 0 in
if b=0 then printfn "b is 0"

これはよくあるif文の使い方に近いです。


C#などのelse ifのように条件式をいくつか書いて一致したところの式を戻り値としたい場合、現時点の説明では下記のようにelseの式にさらにif式をネストして、ひとつずつ条件式を足していきます。
(else ifという構文ではありません。)

let en = "Japan"
let ja =
  if en="USA" then "アメリカ"
  else if en="Japan" then "日本"
  else if en="France" then "フランス"
  else "その他の国"

しかし、elifというキーワードを使用することで上記コードは下記のように書き換えられます。

let en = "Japan"
let ja =
  if en="USA" then "アメリカ"
  elif en="Japan" then "日本"
  elif en="France" then "フランス"
  else "その他の国"

elifを使うif文は、最初は普通のifで書いて、以降はelifと書いて条件式、thenを書いて式を書いていきます。
これは条件式をひとつずつ評価していって、最初に条件を満たしたところの式(どれも条件を満たさなければelseの式)が戻り値となります。
それ以降の条件式は評価されません。

ここでも戻り値とならなかった式は評価されないのと、最後のelseは戻り値がunit型の場合は省略できます。

ただ、この場合のコードはC#などではswitch文に置き換えるほうが望ましいのと同じく、F#においては次回に紹介するパターンマッチというものに置き換えるほうがベターでしょう。



for式

F#でループ処理を記述したい場合はfor式かwhile式を使用することになりますが、ここではfor式のみ紹介します。

for式は式といっても戻り値はunit型です。
for式には2種類あるのですが、まず1種類目を紹介します。

for i = 0 to 4 do printfn "%d" i done

このようにforを書いて、ループ変数名を書いて=を書き、ループ変数の最初の値 to 最後の値と書きます。
そしてdoと書いて、ループ処理の本体となる式を書いてdoneを書きます。

F#の変数は不変であると前述しましたが、このループ変数は不変でない特別なint型の変数です。

この式はループ変数にはまず最初の値が入って、本体の式を評価します。
次にループ変数が1つインクリメントされ、もう一度、本体の式を評価します。

このようにループ変数のインクリメントと本体の式の評価を、ループ変数が最後の値より大きくなるまで繰り返します。

printfn "%d" iというのはprintfn関数に整数を出力させたいときに、こう書きます。


次に2種類目のfor式です。
C#でいうところのforeach文に相当します。

for item in ["Sunday";"Monday";"Tuesday"] do printfn "%s" item done

forを書いて、変数名を書いてinと書き、コレクションを書いてdo ループ処理の本体の式 doneと書きます。

コレクションというのは、ここでは次回に紹介するリストを置いていますが、私がF#の勉強に使ったサイトによるところでは、リストやSeqというもの、およびGetEnumeratorメソッドがある型を置くことができるそうです。

なお、変数名は実はこれも次回に紹介するパターンが正体です。
こちらも特別で不変ではありません。


この式は変数に、置いたコレクションの最初の要素がまず入って、本体の式を評価します。
そして次のコレクションの要素が変数に入って、再び本体の式を評価します。

これをコレクションの最後の要素まで繰り返します。


また、for式の戻り値はunit型だと前述しましたが、実は次回に紹介するリストを戻り値として返す形式のfor式があります。

let squares = [for item in [0;1;2;3] -> item * item]

全体を[]で囲むのですが、中にforを書いて変数名を書いてinを書いてコレクションを書くところまでは前述の2種類目のfor式と同じです。
続けてdoではなく->と書いて、次に本体の式を書いておしまいです。

これは前述の2種類目のfor式と同じく、変数にコレクションの要素をひとつずつ入れて本体の式を評価していくのですが、本体の式の値を、評価した順番でリストにして返します。




今回は前編ということで以上です。
読むのは短いかもしれませんが、これでも執筆に数時間×3日を費やしました。

次回は後編ですが、データ構造を解説しなければならないうえ、パターンや判別共用体や例外が濃い内容なので
、今回よりもめちゃくちゃ長くなると思いますね。

なので今後すぐに後編の執筆を始めたとしても、近いうちに公開というわけにはいかなさそうです。
ゆっくりとお待ち下さい。

もうちょっと前編を長くして、後編を短くしてもよかったかもしれませんね~。

tag: プログラミング F# 関数型 関数 勉強まとめ

コメント

コメントの投稿

トラックバック

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

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