ガジェット通信

見たことのないものを見に行こう

【関数型言語Haskellの基本】高階関数map,reduceを使って遊んでみた

DATE:
  • ガジェット通信を≫

Haskellの対話型環境で関数プログラミング

プログラミングには「命令型」と「関数型」があって、俺たちが触れてきたのはどうも前者らしいぞ…と耳打ちされたあなた。
でも、両方触れたことがなければ何のことやら…ですよね。

この記事では、「関数型言語」が気になるけれど、学んだことはまったくないという方へ、Haskellの簡単なサンプルコードを使ったエクササイズを提供します。
対話型環境で進めますので、一歩ずつ手ごたえを感じながら学ぶことができますよ。

関数プログラミングの特徴って?

はじめに、Haskellのような、典型的な関数プログラミング言語の特徴をザックリ「ある/なし」で表現してみます。

ある
なし
式(expression)
文(statement)
関数型オブジェクト
boolやintなどのプリミティブ型
永続的なデータ
破壊的代入

これだけでは、よく分からないかもしれません。
以下のCのコードを対象に、命令型言語と関数型言語の違いを見ていきましょう。

#define n 5
int main(){
int i = 0;
int snakes[n] = {5,4,3,2,1};
int flogs[n] = {7,6,1,2,2};
int survivors = 0;
while(i snakes[i]){
survivors += (flogs[i] – snakes[i]);
}
i++;
}
printf(“%dn”, survivors);
return 0;
}

カエルの配列flogsと、ヘビの配列snakesがあり、5つのグループ0,1,2,3,4に何匹ずつ存在するかを表す配列を定義します。
そして、「各グループ内のヘビはカエルを一匹ずつ食べる」という設定で、生き残るカエルの総数を求められるようプログラムを書きました。

int snakes[n] = {5,4,3,2,1};
int flogs[n] = {7,6,1,2,2};

たとえばsnakes[0]とflogs[0]が、グループ0にいるヘビとカエルの数を表します。
それぞれ’5′と’7′ですから、このグループではヘビがカエルを一匹ずつ食べて、カエルは2匹生き残ります。

int survivors = 0;
while(i snakes[i]){
survivors += (flogs[i] – snakes[i]);
}
i++;
}

減算で生き残るカエルの数を計算し、survivors変数に加えていきます。
ちなみに、ヘビの数がカエルの数より多い時は0匹となるよう、if文で制御しています。

実行すると、「5」が出力され、5匹のカエルが生き残るとわかります。
このプログラムを上記の「ある/なし」の表と照らしてみましょう。

末尾にセミコロンのあるものは全て代入、出力などの「命令文」ですから、実はこのコード中にある式はwhile文の条件式(i < n)のみ。ほかは全て文にあたります。

さらに、i, sumなどの「プリミティブ型変数」も使わず、while文内の処理のような「破壊的代入」も行わないのです。つまり、上記コードのほとんどの要素はHaskellに「ない」のですね。

どうやって同じ処理を実現するかが気になりますか?上記と同じ機能のプログラムをHaskellで書いてみましょう。

Haskellで書くとこうなる

ここからは、対話型環境を使うことを想定して記述します。
GHCの対話環境であるGHCiで実行するのが一般的ですが、オンラインでも下記サイトなどで実行することができます。

http://ghc.io/

まず、リストを用意します。

let snakes = [5,4,3,2,1]
let flogs = [7,6,1,2,2]

let A = B という書き方は、対話型環境でのHaskellプログラミングでよく使われます。

英語のletと構文は同じです。「AをBと等価たらしめる」すなわち「AをBと定義する」と覚えておくとよいでしょう。上記コードは、snakes配列を[5,4,3,2,1]と定義し、flogs配列を[7,6,1,2,2]と定義しています。

次に、「ヘビがカエルを食べた結果、生き残るカエルの数」を返す関数eatを定義します。
letは、関数定義にも使うことができます。

let eat (s,f) = if f > s then f-s else 0

ここでifが登場しますが、Haskellにおけるifは「if文」ではなく、値を返す「if関数」です。徹底的に関数の組み合わせで処理を書いて行きます。

関数に関数を渡す「高階関数」登場

次に登場するのが、関数プログラミングの花形「高階関数」。
高階関数とは「関数を引数に取る関数」のことで、破壊的代入や、forループ、whileループのような繰り返しの記述なしにさまざまな処理を実現します。
これが、関数プログラミングにおけるもっとも特徴的なメソッドです。

これから使うmapという高階関数は、第一引数に関数を、第二引数にリストを取り、第二引数のリストに対し第一引数の処理を適用します。
少々ややこしいですから、実際にやってみましょう。

第一引数として渡す関数はもちろん、先ほど定義したeatです。
第二引数にはsnakes,flogsの二つのリストを渡したいのですが、mapに渡せるリストは一つだけですから、zipという関数で一つのリストにまとめます。

let snakeFlog = zip snakes flogs

中身は以下のようになっています。

snakeFlog
[(5,7),(4,6),(3,1),(2,2),(1,2)]

いよいよ高階関数mapを使います。
第一引数として関数を、第二引数としてリストを渡しましょう。

結果は生き残った各グループのカエルの数を表すリストになりますから、返り値はsurvivorsというリストを定義して、そこに格納することにしましょう。

let survivors = map eat snakeFlog

リストの中を覗いてみましょう。

survivors
[2,2,0,0,1]

全部で何匹生き残ったか、ここまで来れば一目瞭然ですが、すべての値を足し合わせるメソッドを実装したいですね。

足し算の関数”+”をリストに対し繰り返し適用するために、高階関数foldlを使ってみましょう。
二つの引数しか取る事のできない関数”+”を繰り返し適用したい場合、for文のないHaskellにおいてはreduce(畳み込み)というメソッドを使います。

reduceとはリスト等の引数を一つの値に集約させることを意味する、抽象的な概念で、関数型言語にはこれを実現するための様々な高階関数が存在します。

二つの引数を取る関数+を使ってsurvivorsの全要素を足し合わせるためのアイデアの一つとして、以下のような方法を思いつくでしょう。

まず初期値0に第一要素を足し合わせ、その結果に第二要素を足し合わせ、その結果に対して第三要素を足し合わせ…という以下のような数式です。

((((0 + 2) + 2) + 0) + 0) + 1

foldl関数はこのような、特定の関数を使った繰り返し操作を実現する高階関数です。

foldlという名前は、「畳む」を意味するfoldと、「左」を意味するlの組み合わせです。「畳み込み(reduce)を左から適用していく」ために付きました。

皆さまお察しの通り、「右」、つまりリストの末尾から関数を適用したい場合は、foldrという関数が使えます。

さて、リストsurvivorsに対して、(+)を繰り返し適用してみましょう。foldlは第一引数に関数、第二引数に初期値、第三引数にリストを取ります。

foldl (+) 0 survivors
5

これで「5」という出力が得られ、5匹のカエルが生き残ることが分かりました。

関数型プログラミングの良さ

ここまでの記述を総合すると、Cよりもはるかに短くシンプルに書けていることが分かると思います。

さらに、forループなどは登場せず「関数を関数に渡す」高階関数が設計の基本ですから、一つ一つの関数がどんな機能を持っているか整理しないと、コーディングが成立しません。

結果的に、簡潔かつ保守性の高く、スケーラブルな実装を実現しやすいと言えるでしょう。
なお、スケーラビリティを冠した関数型言語に「Scala」があります。こちらの記事もございますので、ぜひのぞいてみてくださいね。

カテゴリー : デジタル・IT タグ :
CodeIQ MAGAZINEの記事一覧をみる ▶
  • 誤字を発見した方はこちらからご連絡ください。
  • ガジェット通信編集部への情報提供はこちらから
  • 記事内の筆者見解は明示のない限りガジェット通信を代表するものではありません。

TOP