関数型プログラミングはまず考え方から理解しよう

  • 1484
    いいね
  • 14
    コメント
この記事は最終更新日から1年以上が経過しています。

関数型プログラミングが注目されて久しいです。
そろそろ勉強しておかないとなぁということで調べてみるものの、情報として出てくるのは"高階関数","カリー化","遅延評価","モナド"などの物々しい単語の数々。これらを勉強して理解した気になったものの、プログラムを書こうと思うと全然書けませんでした。
結局、関数型言語を実現する手段を学ぶ前に関数型と言われるパラダイムを理解しないことには書けません。ということでここでは関数型プログラミングの"手法"ではなく、オブジェクト指向と関数型プログラミングを比較しながら考え方の違いを見ていきます。

本日の例題

プログラミング課題を設定してオブジェクト指向と関数型で解答してみましょう。

課題:
唐揚げ弁当がいくつかあるとします。それぞれ唐揚げが複数入っています。
この中からx個の唐揚げをつまみ食いするプログラムを作りましょう。
つまみ食いはバレないようにするために、
その時点で最も唐揚げ数が多いお弁当から取るという仕様にします。

※仕様の細かいツッコミはご勘弁を…。

オブジェクト指向で解く

みんな大好きオブジェクト指向では、物単位で考えます。言わずもがなですね。やり方はいろいろあると思いますが、まずお弁当という単位で集めると

唐揚げお弁当クラス

  • 状態
    • 主食名
    • 唐揚げ個数
    • 値段
  • 振る舞い:
    • つまみ食い()
    • 値段取得()
    • 唐揚げ個数取得()

のような感じで、弁当に必要な要素と必用な処理をひとまとまりにして扱うというのがオブジェクト指向です。
お弁当スーパークラスを作って継承で唐揚げ弁当クラスを、というように拡張を考えた設計にするというのも汎用性が必要な場合は良いと思います。

では早速コードにしてみましょう。
言語はJavaScriptにしています。

オブジェクト指向
(function(){
    window.onload=function(){
        //////////////////
        //  クラスの定義  //
        //////////////////
        //弁当クラス
        var Bento = function(dish,num){
            //おかず名
            this.dish = dish;
            //何個入っているか
            this.num = num;
        };

        //中身の表示関数
        Bento.prototype.show=function(){
            console.log(this.dish +this.num + "個");
        };

        //////////////////
        //  データの準備  //
        //////////////////
        //3個弁当を作るとする。
        var karaben1 = new Bento("唐揚げ",10);
        var karaben2 = new Bento("唐揚げ",8);
        var karaben3 = new Bento("唐揚げ",6);

        //全お弁当配列
        var order = [karaben1,karaben2,karaben3];

        //////////////////
        // つまみ食い実施 //
        //////////////////
        //つまみ食いする数
        var eat = 5;

        //eat回繰り返す
        for(var loop = 0; loop < eat;loop++){
            //唐揚げが最大個数の弁当を探す
            var maxIndex = 0;
            for(var i = 0; i < order.length; i++){
                maxIndex = order[maxIndex].num < order[i].num ? i : maxIndex;
            }
            //一個食べる
            order[maxIndex].num--;

            //表示
            console.log("-------");
            for(var i = 0; i < order.length; i++){
                order[i].show();
            }
        }
    }
}());

/* ↓出力↓ */
-------
唐揚げ9個
唐揚げ8個
唐揚げ6個
-------
唐揚げ8個
唐揚げ8個
唐揚げ6個
-------
唐揚げ7個
唐揚げ8個
唐揚げ6個
-------
唐揚げ7個
唐揚げ7個
唐揚げ6個
-------
唐揚げ6個
唐揚げ7個
唐揚げ6個

まずお弁当クラスに今回必要な変数と表示用のメソッドを定義します。
そして個数は適当で3つほどインスタンスを生成して、そこから5つつまみ食いする想定です。エラー処理等は省略。

ではこれを関数型の考え方に変えていこうと思います。

関数型バージョン

関数型プログラミングで考えること

関数型プログラミングの考え方は、「データに何らかの処理を加えていく」の連続で組み立てていくものです。プログラムの関数と言うより数学の関数をイメージするといいかもしれません。

データ ⇒ f(データ) ⇒ データ´ ⇒ g(データ´) ⇒ データ´´

これだけ考えると、当然かと思いがちですが、オブジェクト指向はこれとは異なる考え方です。インスタンスという塊の中に状態(変数)を持っており、それを参照したり変更するため、同じ関数を呼び出しても異なる答えが返ってきます。
これを"副作用がある"と表現するようです。
このように何か処理を実行した際に、入力として受けつけたデータ以外の物が変化することを"副作用がある"と表現するようです。

補足:副作用の説明として不適切だったため修正。
同じ物を参照して同じ答えが返ってこないのは「参照透明ではない」状態です。
参照透明性…同じ入力なら必ず同じ結果を返す性質

関数型言語はこの副作用のないプログラムを目指します。

関数型に書き換えるためのステップ

  1. 状態と振る舞いを切り離す
  2. ループではなくmapやreduceを使う
  3. 振る舞いを副作用のない関数にバラす

状態と振る舞いを切り離す

まず、状態と振る舞いを持つクラス(今回はJSなので関数ですが)を分解します。

分解
//データ部分のみ定義
var order = [{dish:"唐揚げ",num:10},
             {dish:"唐揚げ",num:8},
             {dish:"唐揚げ",num:6}];
//表示関数
var show = function(d){
    console.log(d.dish +d.num + "個");
};
//中身を全て表示
var showAll = function(data){
    data.forEach(show);
};              

先程はBentoクラスとして定義していた部分を、データだけにしました。表示部分も独立したものを作っています。

ループではなくmapやreduceを使う

オブジェクト指向の例で出てきている

ループ例
//唐揚げ数最大を探す
var maxIndex = 0;
for(var i = 0; i < order.length; i++){
    maxIndex = order[maxIndex].num < order[i].num ? i : maxIndex;
}
//一つ食べる
order[maxIndex].num--;
//表示
console.log("-------");
//console.log("つまみ食い"+loop+"個目");
for(var i = 0; i < order.length; i++){
    order[i].show();
 }

このようなループをなくさなくてはいけません。ループは"i"の状態とそれに伴う結果がどんどん変わっていく副作用の代表だからです。
これにはmapやreduceなどのイテレーションメソッドを活用します。
上記のループ部分を書き換えてみましょう。

map/reduce書き換え
//唐揚げ数最大
var maxBento = order.reduce(function(a,b){return a.num < b.num ? b : a;});
//一つ食べる
maxBento.num--;
//表示
console.log("-------");
order.forEach(function(o){o.show()});

map/reduceの細かい使い方は他に任せますが、このように見た目もかなり短縮できます。
しかし上記のプログラムでは副作用が消えていません。maxBentoの中身を上書きしていますし、そもそもorderの中身を変えては表示して、という考え方になっています。

副作用のない関数にバラす

関数型の考え方にそって副作用をなくしていきます。
ここはシンプルに「引数でデータを受け取り、それ以外の情報を使わずに戻り値を返す」関数を作ることを考えましょう。

今回はこのようにバラして見ました。

  • 唐揚げ数が最も多い弁当を見つける
    • 入力:全ての弁当配列
    • 出力:最も唐揚げ数が多い弁当
  • 唐揚げを一つ食べる
    • 入力:全ての弁当配列
    • 出力:唐揚げ数が多い弁当の唐揚げ数を一つ減らした弁当配列
  • 指定個数唐揚げを食べる
    • 入力:あといくつ唐揚げを食べるか、全ての弁当配列
    • 出力:画面へ表示

たぶん、ソースコード見たほうが早い気がします。

関数型
(function(){
   window.onload=function(){
        //////////////////
        //  データの準備  //
        //////////////////
        var order = [{dish:"唐揚げ",num:10},
                     {dish:"唐揚げ",num:8},
                     {dish:"唐揚げ",num:6}];

        //////////////////
        //  関数定義   //
        //////////////////
        //表示
        var show = function(d){
            console.log(d.dish +d.num + "個");
        };

        //中身を全て表示
        var showAll = function(data){
            data.forEach(show);
        };

        //つまみ食いする弁当を決定する
        //今回は唐揚げが最も多い弁当を見つける
        var selectEatingBento = function(data){
            return(data.reduce(function(a,b){return a.num < b.num ? b : a;}));
        }

        //選んだ弁当の唐揚げを一つ食べる
        //弁当配列をmapでコピー。唐揚げ数が最大の弁当に対してはnum-1を設定。
        var eatingKaraage = function(data){
            return(
                data.map(function(d,i,arr){
                    if(d === selectEatingBento(arr)){
                        return({dish:d.dish,num:d.num-1});
                    }else{
                        return(d);
                    }
                })
            );
        }

        //メイン処理
        //num:あと何個たべるか
        //data:全弁当データ
        var eating = function(num,data){
            //表示
            console.log("-------");
            showAll(data);
            //まだ食べる場合、再帰で呼び出し
            if(0 < num){
                eating(num-1,eatingKaraage(data));
            }
        };

        //実行
        eating(5,order);
    }
}());

メイン処理の部分で、再帰処理を行っています。あと何個食べるか、という情報が再帰のたびに減りながらメイン処理を呼び出すことでループ処理を置き換えています。

とりあえずこれで副作用を抑えた関数型プログラムになりました。

まとめ

  • オブジェクト指向と関数型プログラミングの違い
    • データと振る舞いをどう扱うか
    • 副作用があるか
    • ↑乱暴すぎるので訂正:オブジェクト指向ではカプセル化により副作用を隠蔽してわかりやすくする、関数型ではそもそも副作用を最小に抑えるように組み立てていく。
  • 関数型にするには
    • データと振る舞いを分離させる
    • 外部に依存しないよう関数の入出力を定める
    • 再帰やイテレーションメソッドを使い、副作用を取り除く

関数型を採用するメリットはいろいろなところで言われていると思いますが、一番は参照透明性による処理内容の明確化かなと思います。入力が決まれば結果が一意に決まるというのは数値計算の多い処理には有難いですし、テスト全般が楽になります。テスト駆動開発なんかとも仲がいいのじゃないでしょうか。

参考

関数型プログラミング入門…非常に参考になった。というかここに触発されて書いた。
JavaScriptで関数型プログラミングの入門
関数型言語のウソとホント