関数型プログラミング

目次

概要

F#はマルチパラダイム言語ではありますが、コア互換のあるOCamlと同じく関数型ベースの言語です。

関数型言語は基本的に副作用を嫌います。 副作用を全く持たないものを純粋関数型とよび、副作用をもつことができるものを非純粋関数型言語といいます。F#は非純粋関数型言語です。

副作用とは、状態変化があり、以降の処理の結果を変えてしまうことをいいます。副作用の例として、変数の代入があり同じ変数を使った処理でも値(状態)が変わることで処理の結果は変わってしまう。

ちなみに、副作用を持たず同じ条件下では同じ結果が得られ、他の処理に影響を与えない性質を参照透過性という。関数型プログラミングでは参照透過性を保つことが良いプログラムとされる

let宣言

値や関数の宣言するには、letを使用します。

        書式
        let x = expr 
        

exprは式(値や関数もexpr)です。 変数 x に、exprを束縛するといいます。 具体的な例を見ていきましょう

> let one = 1;;
val one : int


その後、1を束縛したoneは、1と評価される。

>  one;;
val it : int = 1


C言語などでは、コンパイラに型を指定する必要があるが、
F# は静的に型推論する。

> let one = "one"
val it : string



F# では.NET の型をしようするが、それぞれ省略型が用意されている
ここでは主なものを紹介する

 

type int (int32) System.Int32 32bit整数型
type bool System.Boolean ブール型(真偽)
type string System.String Unicode文字列
type byte System.Byte 8bit符号無し整数
type float32 System.Single 32bit 浮動小数
type float System.Double 64bit 浮動小数
type decimal System.Decimal 非常に大きな整数(財務計算などで使う
type exn System.Exception Exception(例外)型
type obj System.Object, .NET Frameworkでは全てのクラスがObjectを継承する

数値では、u を付けることで符号無しとなる。


type uint32 など

unit型

値がないことを表します。 (C言語などのvoid型にあたります)

> ();;
val it : unit = ()

 

> printf "hello world\n";;

hello world

val it : unit = ()


関数

論理的にまとまった処理(手続)は、関数としてまとめることができます。関数型言語では、入力に対して出力が(基本的には※1)あることが基本です。 数学の関数に近い関数といえるでしょう

※1 例外的に、unit(値がない)を返すことができます

> let sum x y = x + y;;

val sum : int -> int -> int


引数を与えて、関数を評価してみる

> sum 1 2;;

val it : int = 3


関数は下記のようにも書くことができる。

> fun x y -> x + y;;


束縛すれば、後で再利用できる。

> let sum = fun x y -> x + y;;

条件分岐

if

書式

if 条件式 then 条件式が真の時に実行する式 else 条件式が僞の時に実行する式

> let compare x y =  
        if x > y then x else y;;

 

val compare : 'a -> 'a -> 'a



else は省略できる

if 1 > 0 then printf "1は0より大きい";;


パターンマッチ

match文

パターンマッチによる条件分岐です。
例として、数値を単語(文字列)に変換を定義します。
今回はソースファイルを作成し、コンパイルして実行してみます。

MatchSample.fs

let NumToString n =
    match n with
        | 1 -> "one"
        | 2 -> "two"
        | _ -> "another number"
        printf "%s\n" (NumToString 2);;
        printf "%s\n" (NumToString 3);;


実行結果

two
another number

NumToString の引数に2を与えると、パターン 2 にマッチし、"two"を出力しています

NumToString の引数に3を与えると、パターン _ にマッチし、"another number"を出力します
※ _ はその他全てにマッチする


ちなみに、match は下記のように省略して書けます

function
    | 1 -> "one"
    | 2 -> "two"
    | _ -> "another number"

さまざまなパターンマッチ

リストのパターンマッチ

工事中

asパターン

asパターンを使うと、パターンマッチした値全体を参照できます。

下記はConsオペレータでリストの先頭と残りを分解したパターンマッチですが、 asパターンを使うことによってリスト全体も参照しています(as listの部分がそれです)。

#light

let AsPattern = function 
    [] -> printfn "no match"
    | head::tail as list -> // リストの先頭
    			    printfn "head=%d" head 
    			    
    			    // 先頭をとった残りのリスト
    			    printf "tail=[" 
    			    ignore (List.map (printf " %d") tail)
    			    printfn " ]"
    			    
    			    // リスト全体 ( as listにより参照
    			    printf "list=[" 
    			    ignore (List.map (printf " %d") list)
    			    printfn " ]"
    			  
AsPattern [1;2;3]

実行結果

head=1
tail=[ 2 3 ]
list=[ 1 2 3 ]

コメント

コメントはコンパイルの対象外になります。

ソースコードは後から読み返し安く書くことを意識しなければなりません。
コメントは、その時の助けとなります。

 

複数行コメント

(**)

(*
 複数行コメント
   この部分はコンパイル対象外となります。
*)

一行コメント

//

// 一行コメント

リスト

リストは、同じ型の値を並べたものです。
リストは多相型なので、整数、文字、タプルなど、さまざまな要素を持つことができます。

要素1, 2, 3をもつリストを定義してみます。

> let list = [1;2;3];;

評価結果

val list : int list

Consオペレータ

リストは、Consオペレーターを使って下記のようにも書けます

let list = 1::2::3;;

Consオペレーターは、リストの先頭に要素を加えるための演算子です。
パターンマッチで、リストを先頭要素と残りの要素に分離するときも使います。



空リスト

何もデータが入っていないリストです。

> let empty_list = [];;
val empty_list : 'a list
'a というのは型変数といい、
型を指定(もしくは推論)されてるのを待っている型です。
C++でいう、テンプレートと同じようなものです。
ここらへんは、後で詳しく解説しようと思います。

リスト操作

リストに対するさまざまな操作を紹介します。

リストの結合

2つのリストを1つのリストに結合します。

> ["one"] @ ["two"];;

val it : string list = ["one"; "two"]

再帰

工事中

レコード

レコードとは、論理的に関連のあるデータをまとめたものを
新たなデータ型(レコード型)として定義したものです。
C言語をご存知の型なら、構造体にあたる概念です

下記に使用例を示します

// 生徒を表すレコードを定義します。
type student =
{
	name: string;
	grade: int;
};;

// student型の値を作成してみます。
let highschool_student = 
{
	name = "taro";
	grade = 11;
};;

// レコードのそれぞれのフィールドにアクセスしてみます。
printf "name=%s,grade=%d\n" highschool_student.name highschool_student.grade;;

バリアント

バリアントは、論理的に関係のある値を統一して扱うのに便利な機能です。

※OCamlではバリアントという名称ですが、F#では Discriminated Unionとして紹介されているので、  今後そちらに用語を統一するかもしれません。

さっそく、サンプルコードといきたいと思います

私が好きなプログラミング言語を列挙し、FavoriteProgramLanguage型として定義します そして、パターンマッチによりそれらの値に対応する文字列を取得します。

#light

type FavoriteProgramLanguage = FSharp | CSharp | CPlusPlus | Perl | Java

let GetString = 
    function
          FSharp    -> "FSharp"
        | CSharp    -> "CSharp"
        | CPlusPlus -> "C++"
        | Perl      -> "Perl"

printfn "%s" (GetString FSharp)

ここで、Java がパターンに含まれていないことにお気づきでしょうか?  実はこのプログラム、コンパイルするとWarningが発生します。
   なぜでしょうか。 バリアント型として定義された型をパターンマッチする場合、  すべての値がパターンマッチによって調べられていないとコンパイラが警告してくれるからです。  このことにより、値を追加した時にの実装忘れによるバグが未然に防がれます。 強力な静的型付け言語であるF#の本領発揮というわけです。


オプション型

手続き型やオブジェクト指向型の言語の多くでは値の有無をNULLという特別な値をもって表現します
変数にNULLが代入されている場合、値が無く、その他の場合は有るというわけです。
int a = 5     // aには5という値がある。
int b = NULL; // bは値を持っていない。
ただ、このNULL, 定数 0 の別名として定義されており、
0という値があることを表現したい場合は、別の値をNULLに割り当てる必要があります。
例えば -1などです。しかしすぐに、-1 と 0を使いたい場合は・・・という問題にぶち当たることはもうお気づきでしょう。
このような問題はF#ではありません。解決される仕組み、Option型が用意されているからです。

今回は前置きが長くなってしまいました。
さて、F#のOption型ですがはSome(値がある)かNone(値がない)どちらかをとります
さっそく、Option型のサンプルを以下に示します。

Someのサンプルです
> let value = Some(5);; val value : int option

このint option型の値の範囲は、
Some(intの値の範囲 -2,147,483,648 ~ 2,147,483,647 )とNoneを合わせたものになります。
(これで、冒頭の問題が解決されることがわかっていただけたと思います)

Noneのサンプルです。
> let noValue = None;;
val noValue : 'a option
今回の結果が多層型のオプションとなっています。 これは、Noneは、どの型でも利用されるので、型ごとに定義する必要があるのですが このように多相型として定義されているのでその必要がなくなるというわけです。

値が有無をチェックする関数を定義します。
> let checkValue val = 
	  match val with 
		  | Some(_) ->true 
	      | None -> false;;
	    
val checkValue : 'a option -> bool
checkValue関数を使って、先ほど定義したvalue変数とnoValue変数それぞれについて、
値の有無をチェックしてみます。
> checkValue value;;
val it : bool = true

> checkValue noValue;;
val it : bool = false
このようにOption型は値が設定(初期化)されているのか、
されていないのかのチェックなどに利用すると良いでしょう。

カリー化

カリー化とは引数を2つ以上とる関数について、下記のような関数を作る技法です。

・引数
 関数のひとつ目の引数
・戻り値
 もとの関数の残りの引数をとり結果を返す関数

カリー化の例を示します。

> let add a b = a + b;;
val add : int -> int -> int

次に、先ほどのadd 関数をカリー化した例を示します。

> let add a = fun b -> a + b;;
val add : int -> int -> int

ひとつめの引数 a を受け取って、残りの引数 b をとり、結果(a+b)を返す関数として定義しました。

部分適用

工事中

高階関数

関数を引数に取ったり、関数を結果として返す関数を高階関数といいます。
関数型言語では高階関数は非常によく使われます。

代表的な高階関数を紹介します。

fold関数

fold関数はリストの要素を順番に関数で評価していき、結果を蓄積していきます。

1から10までの和を求めます。

> List.fold (fun acc x -> acc + x) 0 [1..10];;
val it : int = 55

下記のように書き直すこともできます。

> List.fold (+) 0 [1..10];;
val it : int = 55

5の階乗を求めます。

> List.fold (fun acc x -> acc * x) 1 [1..5];;
val it : int = 120

先ほどのサンプルと同じように下記のように書き直すこともできます。

> List.fold (*) 1 [1..5];;
val it : int = 120
inserted by FC2 system