JavaScript
es6
関数型プログラミング
es2015

作って学ぶ、ざっくり関数型プログラミング in javascript

4桁の文字列("1060")を時間({m:11, s:00})に変換するapiを作りながら、
関数型プログラミングについて学んで行きたいと思います。

関数型プログラミングとは

参考: JavaScriptで学ぶ関数型プログラミング

関数型プログラミングとは、値を抽象の単位に変換する関数を使用して行うプログラミングであり、
それらを使ってソフトウェアシステムを構築することである

ベルトコンベアーに流れてくるデータに処理を加えていくイメージです。

input(分:秒)   ミリに変換      オブジェクトに変換   
↓                ↓            ↓
1060     ->    660000  ->    {m: 11, s: 00}

純粋性

関数型プログラミングを行うにあたり、純粋な関数であることが重要です。
プログラム内の状態の変更を最小限にする、参照不透明による混乱を避ける、
ざっくり言うと、シンプルにしよう!!ってことです。(KISSの法則)

純粋性には以下のルールがあります。

  • 結果は引数として与えられた値からのみ計算される。
  • 関数の外部で変更される可能性のあるデータに依存しない。
  • 関数実行部の外側に存在する状態を変更しない。

ひとつひとつ見ていきます。

結果は引数として与えられた値からのみ計算される。

つまり、引数が同じならば同じ値を返す。ということです。

まずはBADから

let hoge = 10;
function add(a){
    return a + Math.floor(Math.random() * 100);
}
add(hoge) === add(hoge); // false

上記ではMath.randomをadd関数内で行なっています。
そのため、毎回値が代わり同じ値を返しません。

次にGOOD

let hoge = 10;
let fuga = Math.floor(Math.random() * 100);
function add(a, b){
    return a + b;
}
add(hoge, fuga) === add(hoge, fuga); // false

このような書き方がGOODなのは、テストが非常に行いやすいからです。
テストを行うと、結果として理解しやすく、見やすいコードになります。

NOTE:
テストの有用性については各々調べてみてください。

関数の外部で変更される可能性のあるデータに依存しない。

これはそのままです。

let PI = Math.PI;
function calc(a){
    return a * PI
}
calc(1) //3.141592653589793
PI = 0;
calc(1) //0

関数実行部の外側に存在する状態を変更しない。

まずはBADから

let hoge = [1,2,3];
function head(ary){
    return ary.splice(0,1);
}
head(hoge); //[1]
hoge //[2, 3]

上記ではhead関数内でsplice(破壊的)処理を行い、配列の先頭を取得しています。
そのため、head関数外にあった、hogeの配列が変わってしまっています。

つぎにGOOD

let hoge = [1,2,3];
function head(ary){
    return ary.slice(0,1);
}
head(hoge); //[1]
hoge //[1,2,3]

上記ではhead関数内でslice(非破壊的)処理を行い、配列の先頭を取得しています。
非破壊的なので、hogeは変更されません。

不変性

javascriptには不変性をもった型はほとんどありません。
しかし、文字列は不変性をもった数少ない型です。

let hoge = "string";
hoge.toUpperCase(); //STRING;
hoge //string

しかし以下は許容されています

let 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="_"){
    var 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){
  let 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){
   let result = {};
   let 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を実行させるのは面倒なので、
関数を組み合わせるためのcompoes関数
toMsとtoTimeを順番に行う、composeToTime関数を作っていきます。

NOTE:
関数を組み合わせる関数のことを高階関数と言います。
ここでは割愛するので、各々調べてみてください

//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

次に、compose関数を用いて、toMs関数toTime関数を合成していきます。

// composeToTime
function composeToTime(str){
    return compose(
        (ms)=>toTime(ms),
        (str)=>toMs(str)
    )(str)
}

この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 true;
}

これを先ほどのcomposeToTimeにいれます。

// composeToTime
function composeToTime(str){
    check(str);
    return compose(
        (ms)=>toTime(ms),
        (str)=>toMs(str),
    )(str)
}

compoeToTime(10)  //Error: 型が違う 
composeToTime("10") //Error: 長さが違う
composeToTime("aaaa") //Error: 数字に変換できない

このように、関数を組み合わせてapiとして提供します。

NOTE:
javascriptには厳格な型がありません。
そのため、toMs関数や、toTime関数の入出力には常に危険性が伴います。
しかし、戻り値や引数を毎度毎度、検証するのは大変だし、コードが冗長になってしまします。
そこで、unit testを書くことにより、入出力を少しでも安定させることができます。

まとめ

関数型プログラミングを用いて、簡単なapiを作ってきました。
特に重要だと思うのは、関数がシンプルであることです。
それを実現するために、純粋性を意識すると良いと思います。

またこれ以外にも、メモ化やカリー化、mixinなどがありますが、
これは関数型プログラミングのテクニックのようなもので、高度なことが行えます。
興味があれば調べて見てください。

ではでは、最後までありがとうございました。

参考: JavaScriptで学ぶ関数型プログラミング