*注意
*純粋な関数型プログラミングではありません。
*js特有の妥協をしています。
*詳しくは、頂いたコメントをチェックしてください。
*コメントくださった方々、ありがとうございます!!
4桁の文字列("1060")を時間({m:11, s:00})に変換するapiを作りながら、
関数型プログラミングについて学んで行きたいと思います。
関数型プログラミングとは
関数型プログラミングとは、値を抽象の単位に変換する関数を使用して行うプログラミングであり、
それらを使ってソフトウェアシステムを構築することである
ベルトコンベアーに流れてくるデータに処理を加えていくイメージです。
input(分:秒) ミリに変換 オブジェクトに変換
↓ ↓ ↓
1060 -> 660000 -> {m: 11, s: 00}
純粋性
関数型プログラミングを行うにあたり、純粋な関数であることが重要です。
プログラム内の状態の変更を最小限にする、参照不透明による混乱を避ける、
ざっくり言うと、シンプルにしよう!!ってことです。(KISSの法則)
純粋性には以下のルールがあります。
- 結果は引数として与えられた値からのみ計算される。
- 関数の外部で変更される可能性のあるデータに依存しない。
- 関数実行部の外側に存在する状態を変更しない。
ひとつひとつ見ていきます。
結果は引数として与えられた値からのみ計算される。
つまり、引数が同じならば同じ値を返す。ということです。
まずはBADから
const hoge = 10;
function add(a){
return a + Math.floor(Math.random() * 100);
}
add(hoge) === add(hoge); //false
上記ではMath.randomをadd関数内で行なっています。
そのため、毎回値が代わり同じ値を返しません。
次にGOOD
const hoge = 10;
const fuga = Math.floor(Math.random() * 100);
function add(a, b){
return a + b;
}
add(hoge, fuga) === add(hoge, fuga); // true
このような書き方がGOODなのは、テストが非常に行いやすいからです。
テストを行うと、結果として理解しやすく、見やすいコードになります。
NOTE:
テストの有用性については各々調べてみてください。
関数の外部で変更される可能性のあるデータに依存しない。
これはそのままです。
let PI = Math.PI;
function calc(a){
return a * PI
}
calc(1) //3.141592653589793
PI = 0;
calc(1) //0
関数実行部の外側に存在する状態を変更しない。
まずはBADから
const hoge = [1,2,3];
function head(ary){
return ary.splice(0,1);
}
head(hoge); //[1]
hoge //[2, 3]
上記ではhead関数内でsplice(破壊的)処理を行い、配列の先頭を取得しています。
そのため、head関数外にあった、hogeの配列が変わってしまっています。
つぎにGOOD
const hoge = [1,2,3];
function head(ary){
return ary.slice(0,1);
}
head(hoge); //[1]
hoge //[1,2,3]
上記ではhead関数内でslice(非破壊的)処理を行い、配列の先頭を取得しています。
非破壊的なので、hogeは変更されません。
不変性
javascriptには不変性をもった型はほとんどありません。
しかし、文字列は不変性をもった数少ない型です。
const hoge = "string";
hoge.toUpperCase(); //STRING;
hoge //string
しかし以下は許容されています
const hoge = {fuga: "string"}
Object.assign(hoge, {fuga: "STRING"}) //{fuga: "STRING"}
再帰
指定された回数分"beep_"となる関数を作成してみます。
function chime_for(count){
let sound = "";
for(let i = 0; i < count; i += 1;){
sound += "beep_"
};
return sound;
}
chime_for(3); //beep_beep_beep_
for文を使った方法で期待した通りに動作しています。
しかし、多くの関数型プログラミング言語では、ローカル変数(sound)を変更することができない(不変な)ため、
chime_forのような関数を書くことはできません。
そこでchime_forをローカル変数の変更を用いない再帰関数で行なってみます。
function chime_self(count){
if(count === 0){
return "";
}
return "beep_" + chime_self(count - 1);
}
chime_self(3) //beep_beep_beep_
どちらを使うか
早くて、読みやすい方を選べばよいと思います。
また、javascriptではローカル変数の変更が可能なので、そのダイナミックさに身を任せるのも問題ありません。
chime_x をapiとして提供するにしても、純粋性を守り、副作用がなく、期待した値が返ってくることの方が重要です。
#文字列を時間に変換するapiを作る
準備
ではinput("1060")を{m: 11, s: 0}にする関数について考えていきます。
必要になるのは、
- "1060"をミリに変換する
toMs関数
- ミリを時間の単位をキーにしたオブジェクトに変換する
toTime関数
また、これらの関数を作るにあたり、
- 抽象度をあげる
- 処理は一つに限定する
この2点を意識することで、拡張性、再利用性を向上します。
先ほどの、chime_selfの抽象度をあげてみます。
function chime_for(count = 0, str="beep", glue="_"){
let sound = "";
for(let i = 0; i < count; i += 1){
sound += sound? glue + str: str
};
return sound;
}
chime_for(3, "HELLO","_"); //HELLO_HELLO_HELLO
ささいな違いですが、
引数からsound変数に値を入れるため、好きな文字列を入力できるようになりました。
こうすることにより、chime_selfが便利になります。
関数を作成する
では実際にコードを書いていきます。
// 共通
const base = [60 * 1000, 1000];
//toMs関数
function toMs(str){
const r = []
for(let i = 0; i < 2; i+=1){
r.push(parseInt(str.substr(i * 2, 2), 10));
}
return r.reduce((memo,value,i)=>memo + value * base[i], 0);
}
toMs("1060") //660000
//toTime関数
function toTime(num){
const result = {};
const keys = ["m","s"];
let remaining = num;
for(let i = 0; i < 2; i+=1){
result[keys[i]] = Math.floor(remaining / base[i]);
remaining %= base[i];
}
return result;
}
toTime(660000) //{m: 11, s:0}
関数を組み合わせ、apiとして提供する
いちいちユーザーにtoMsとtoTimeを実行させるのは面倒なので、
関数を組み合わせるためのcompose関数
、
toMsとtoTimeを順番に行う、composeToTime関数
を作っていきます。
NOTE:
関数を組み合わせる関数のことを高階関数と言います。
ここでは割愛するので、各々調べてみてください
//underscoreのcomposeから抜粋
//compose
function compose(){
var args = arguments;
var start = args.length - 1;
return function() {
var i = start;
var result = args[start].apply(this, arguments);
while (i--) result = args[i].call(this, result);
return result;
};
}
compose関数では右から左に関数を実行していきます。
その際、一つ前関数の戻り値を次の関数の引数として実行します。
参考: underscore
es6での関数合成(ライブラリなし)
参考: [JavaScript:関数を合成する関数]
(https://qiita.com/ttatsf/items/ffa0b696bbe11963bd5e)
次に、compose関数
を用いて、toMs関数
とtoTime関数
を合成していきます。
// composeToTime
const composeToTime = compose(toTime, toMs);
このcomposeToTime
をapiとして提供します。
バリデーション
しかし、このままでは引数が、
- 数字に変換できない文字列
- 4桁ではない文字列
などだった場合バグの原因になるので、
それらが引数に与えられた時はエラーを吐くcheck関数
を作ります。
function check(str){
if(typeof str !== "string"){
throw new Error("型が違う")
}
if(str.length !== 4){
throw new Error("長さが違う")
}
if(isNaN(parseInt(str, 10))){
throw new Error("数字に変換できない")
}
return str;
}
先ほどのcomposeToTime
を変更します。
// composeToTime
const composeToTime = compose(toTime, toMs, check);
composeToTime(10) //Error: 型が違う
composeToTime("10") //Error: 長さが違う
composeToTime("aaaa") //Error: 数字に変換できない
このように、関数を組み合わせてapiとして提供します。
NOTE:
javascriptには厳格な型がありません。
そのため、toMs関数や、toTime関数の入出力には常に危険性が伴います。
しかし、戻り値や引数を毎度毎度、検証するのは大変だし、コードが冗長になってしまします。
そこで、unit testを書くことにより、入出力を少しでも安定させることができます。
まとめ
関数型プログラミングを用いて、簡単なapiを作ってきました。
特に重要だと思うのは、関数がシンプルであることです。
それを実現するために、純粋性を意識すると良いと思います。
またこれ以外にも、メモ化やカリー化、mixinなどがありますが、
これは関数型プログラミングのテクニックのようなもので、高度なことが行えます。
興味があれば調べて見てください。
ではでは、最後までありがとうございました。
[JavaScript:関数を合成する関数]
(https://qiita.com/ttatsf/items/ffa0b696bbe11963bd5e)