はじめに
「高階関数を書いたら、中級者になれた気がした」という記事を読んで色々ともやもやしたので批判をしてみる。といった趣旨の記事です。
きっかけはこちらのツイートから⇒https://twitter.com/ababupdownba/status/1241509344329363457?s=20
早速見ていこう
あの記事の流れは、社長の無茶ぶりに頑張って答えながら進んでいくというものだ。
最初の社長の指令はこうだった。
社長「お仕事を持ってきたで」
社長「今日は↓こんな関数を作ってくれ」
- 引数として受け取ったHTML要素の高さを100ピクセルにする
社長「関数の名前はsetHeightで頼むわ」
社長「使うときのイメージとしては↓こんな感じや」
そしてそれに対応したコードがこれだ。
// boxと言うIDを持った要素を取得。constbox=document.getElementById("box");// 関数を呼び出して高さを設定。setHeight(box);
constsetHeight=element=>{element.style.height='100px';};
ここまでは特に悪い所は無い。(強いて言えばsetHeight
という名前は少々わかりずらいので、setElementStyleHeight
とかの方がよさそうといった所)
そして次の社長の指令はコチラ
社長「一つだけ要件を言い忘れてたんや・・・」
- 高さを100ピクセルにして、更に背景色を赤くしたい場合もある
そしてそれに対応して修正したコードがこれだ。
// 高さを100ピクセルにするだけの場合setHeight(box);// 高さを100ピクセルにして、更に背景を赤くしたい場合setHeight(box,true);
constsetHeight=(element,toRed)=>{element.style.height='100px';if(toRed){element.style.backgroundColor='red';}};
このコードにはひとつ問題点がある。
まあ、あえてこのような問題のあるコードにしてあるのは後で高階関数を使うための布石なのだろうが、そもそもこの例では高階関数を使うメリットは微塵もないので指摘していく。
[問題] 関数の命名がおかしいsetHeight
はその名が意味するなら、高さを設定する関数だ。なので、背景色を赤にする処理を記述するのは間違っている。
正しくはこうするべきだろう。
// 高さを100ピクセルにするだけの場合setHeight(box);// 高さを100ピクセルにして、更に背景を赤くしたい場合setHeightAndToRed(box);
constsetHeight=(element)=>{element.style.height='100px';};constsetHeightAndToRed=(element)=>{setHeight(element);element.style.backgroundColor='red';};
しかし、よく考えてほしい。社長はsetHeight
関数を作って欲しいと言っているのだ。
だが、あきらかにsetHeight
は命名がおかしいのでここは頑張って社長を説得するしかない。
とまあ、こんな感じであの記事ではsetHeight
関数にどんどん機能が上乗せされていく。高さを変えるという以外の機能が、だ。
あの記事では最終的にsetHeight
関数の実装は以下の様になる。
constsetHeight=(element,toRed,width)=>{element.style.height='100px';if(toRed){element.style.backgroundColor='red';}// 第三引数のwidthがあったら、要素の横幅を変更するif(width){element.style.width=width+'px';}};
setHeight
関数があまりにも機能を持ちすぎているという問題をあの記事では、コールバック関数というものを使って解決している。
そのコードがこちら。
constsetHeight=(element,callback)=>{element.style.height='100px';callback(element);}
setHeight(box,element=>{element.style.color='green';element.innerHTML='こんにちわ!';});
高さを100px
にすること以外の処理をコールバック関数に任せることで、機能を分割しているのである。
まあ、正直言って酷いコードだ。
[問題]そもそもコールバック関数を使うメリットが無い
コールバック関数なんて複雑で読みにくいものなんか使わずに、以下のようにすればいいだけなのだ。
constsetHeight=(element)=>{element.style.height='100px';}
setHeight(box);box.style.color="green";box.innerHTML="こんにちわ!";
コールバック関数を使う適切な動機ではない。
そして最後にもう一つ気になるのがこちらの説明。
ハスケル子「そうです」
ハスケル子「ちなみにfunc君はここでしか使わない関数なので」
ハスケル子「名前をつけないで無名関数にしてもいいかもですね」ワイ「なるほど」
setHeight(box,element=>{element.style.color='green';element.innerHTML='こんにちわ!';});ワイ「↑こういうことやな」
ハスケル子「ですね」
お気づきだろうか。無名関数とは、function(){}
のように関数名を省略した関数のことである。
しかし、ここで使われているのはelement=>{...}
となっており、これはアロー関数である。無名関数ではない。
問題まとめ
- 例で出てくる関数の命名が適切ではない
- コールバック関数、および高階関数を使うメリットが全くない場合なのに、むりやり使っている。
- 無名関数とアロー関数を混同している。
じゃあ、高階関数を使うメリットってなんなの?
それは、設計において処理の責務と処理呼び出しの責務と処理結果の扱いの責務を分離したいときに使うものである。
例1
まずは処理の責務と処理呼び出しの責務とを分離する例を見てみよう。
以下は5回カウントダウンした後に特定の処理をするFiveCountDowner
クラスの実装と使用の例である。
classFiveCountDowner{constructor(){this.currentCount=0;}update(){this.currentCount++;if(this.currentCount==5)this.invoke();//処理呼び出しの責務を果たしている}invoke(){//処理の責務を果たしているconsole.log("起きて―!!");}}constfiveCountDowner=newFiveCountDowner();fiveCountDowner.update();fiveCountDowner.update();fiveCountDowner.update();fiveCountDowner.update();fiveCountDowner.update();
https://wandbox.org/permlink/PCXFpXrMLlrEPi5I
注目すべきは責務だ。FiveCountDowner
クラスは5回カウント後に処理されるInvoke
関数の実装を持っているし、Invoke
関数がいつ呼び出されるかも管理している。(5回カウントされた後にのみ呼び出すということですね)
これはつまり、FiveCountDowner
クラスは処理の責務も処理の呼び出しの責務も担っているということになる。
もし、5回カウント後に実行される処理内容を柔軟に変えれるようにしようとしたらどうすればいいだろうか?
それは、FiveCountDowner
クラスから処理の責務を分離する必要があることを意味する。
試しにクラスを分けてみようか。
//処理の責務を果たしているclassOkite{invoke(){console.log("起きて―!!");}}//処理の責務を果たしているclassNero{invoke(){console.log("寝ろ");}}classFiveCountDowner{constructor(processObject){this.currentCount=0;this.processObject=processObject;}update(){this.currentCount++;if(this.currentCount==5)//処理呼び出しの責務を果たしているthis.processObject.invoke();}}constfiveCountDowner1=newFiveCountDowner(newOkite);fiveCountDowner1.update();fiveCountDowner1.update();fiveCountDowner1.update();fiveCountDowner1.update();fiveCountDowner1.update();constfiveCountDowner2=newFiveCountDowner(newNero);fiveCountDowner2.update();fiveCountDowner2.update();fiveCountDowner2.update();fiveCountDowner2.update();fiveCountDowner2.update();
https://wandbox.org/permlink/jebwILGCsiwvnn9e
このようにして、責務を分割して処理を柔軟に変更することができた。
しかし、考えても見てほしい。いちいち新しい処理が増えることにOkite
とかNero
とか毎回クラスを作ったりするのは面倒ではないか?
そこで高階関数を使う。
以下が高階関数を使ったversionだ。
classFiveCountDowner{constructor(callback){this.currentCount=0;this.callback=callback;}update(){this.currentCount++;if(this.currentCount==5)this.callback();}}constfiveCountDowner1=newFiveCountDowner(()=>console.log("起きて―!!"));fiveCountDowner1.update();fiveCountDowner1.update();fiveCountDowner1.update();fiveCountDowner1.update();fiveCountDowner1.update();constfiveCountDowner2=newFiveCountDowner(()=>console.log("寝ろ"));fiveCountDowner2.update();fiveCountDowner2.update();fiveCountDowner2.update();fiveCountDowner2.update();fiveCountDowner2.update();
https://wandbox.org/permlink/cQmlfjq0OQu9PJxB
このように、関数にクラスを渡すのではなく、関数を渡す。関数に関数を渡すことが高階関数というものである。
例2
例1では処理の責務と処理呼び出しの責務とを分離する例だった。ここにさらに、処理をした結果の責務について考えていこうと思う。
そのために、以下の問題を例として使う
- 数値の配列の要素一つ一つを倍にした配列を作るdoubleArray関数を実装してほしい。元の配列を直接変更しても構わない。
- 数値の配列の要素一つ一つから-10にした配列を作るtenMinusArray関数を実装してほしい。元の配列を直接変更しても構わない。
まずは何も考えずに実装してみよう。
constdoubleArray=(array)=>{for(leti=0;i<array.length;i++)array[i]=array[i]*2;returnarray;};consttenMinusArray=(array)=>{for(leti=0;i<array.length;i++)array[i]=array[i]-10;returnarray;};console.log(doubleArray([1,2,3,4]))console.log(tenMinusArray([10,20,30,40]))
https://wandbox.org/permlink/77zhhqzApnmMyH2X
このコードには無駄がある。まずはそれを見つけるために処理を分解していこう。
- 配列の中身を1つずつ取り出すfor文
- array[i]=の要素更新処理結果を代入する
- 要素を更新する処理
array[i]*2
とarray[i]-10
- 結果を返すreturn文
要素を更新する処理以外は共通する部分だ。
処理の責務を要素を更新する処理。
処理呼び出しの責務をfor文内の処理。
処理をした結果の責務を要素更新処理結果を代入する処理と考える。
そうして、処理の責務だけを関数から分離したmapArray関数を高階関数を使って実装してみよう。
constmapArray=(array,callback)=>{for(leti=0;i<array.length;i++)array[i]=callback(array[i]);returnarray;}constdoubleArray=(array)=>mapArray(array,x=>x*2);consttenMinusArray=(array)=>mapArray(array,x=>x-10);console.log(doubleArray([1,2,3,4]))console.log(tenMinusArray([10,20,30,40]))
https://wandbox.org/permlink/QXzZ5Vs6XjxSBNsE
例2(オマケ)
例2はカリー化と部分適用を使ってさらに綺麗に美しく書くことができる。
まあ、これを美しいと感じるかどうかは人によるが。
constmapArray=callback=>array=>{for(leti=0;i<array.length;i++)array[i]=callback(array[i]);returnarray;};constdoubleArray=mapArray(x=>x*2);consttenMinusArray=mapArray(x=>x-10);console.log(doubleArray([1,2,3,4]))console.log(tenMinusArray([10,20,30,40]))
https://wandbox.org/permlink/3kqkUYJHuyHPLUR3
高階関数まとめ
高階関数はあくまで、責務を分離する書き方の一つのパターンでしかない。