関数型プログラミングは便利だけど
関数型(以降: FP) は便利だが、オブジェクト指向プログラミング(以降: OOP)や、手続き型プログラミング(以降: IMP)と比べるとスタイルが違うので、導入をためらうこともあるだろう。
いきなりFP にするのではなく、徐々に FPっぽく変更していくのが現実解である。したがって、既存のコードや新しいメンバのプログラミングパラダイムを 徐々に FPっぽく変更していくスキルが必要だし、現実問題として重要だろう。そのための具体的な方針を妄想してみたので垂れ流す。コードはJavaScript。
免責:ある程度 FPの素養がある人を対象としているので、初歩的なことはやらない
#01. 純粋に関数の引数を設計
純粋関数の良いところは、その関数が必要とする全ての情報が関数の引数に明示されている点につきる(少なくとも私にとっては)。あなたが関数を作るときは、それがあたかも純粋関数であるかのように引数を設計しなさい 。実際には副作用やmutationが伴うものであったとしても。
具体例:ユーザ登録内容変更@サーバサイドの処理
フォームデータの内容をバリデートして、変更内容がそのユーザ自身のものであることを確認して、DBにクエリを投げる。普通のコードだとこう。
const modifyUser = (userData) => {
const sessionUserID = getSession().get('userID');
if (userData.userID !== sesseionUserID)
throw Error('Not Authorized!');
if (!isValid(userData))
throw Error('Invalid Data');
const dbSession = getDBsession();
const query = constructQuery(userData);
dbSession.executeQuery(query);
}
よくないのは、この関数がきちんと動くためには多くの依存関係があり、それがコードに隠れてしまっているからだ。このように書いてしまうと、コードを読み解くのが難しいだけでなく、テストがものすごく難しくなる。
以下のように書き換えると非常に見通しが良くなる。テストも簡単。
const modifyUser = (apSession, dbSession, userData) => {
if userData.userID !== apSession.get('userID')
throw Error('Not Authorized!');
if (!isValid(userData))
throw Error('Invalid Data');
const query = constructQuery(userData);
dbSession.executeQuery(query);
}
#02. 業務ロジックと制御構造を混ぜない
プログラムのコードは二つの部分からなる。
- 業務ロジック = やりたいこと
- 制御部 = for,if,一時変数。
この二つの部分が混じるとコードはとても読みにくいものになる。おととい少しバズった私の記事から引用しよう。
// 手続き型
const result = [];
for (item of targetList) {
if (!(isHoge(item) && isMoge(item)))
continue;
result.push(doBaz(doBar(doFoo(item))));
}
// FPを意識
targetList.filter(isHoge)
.filter(isMoge)
.map(doFoo)
.map(doBar)
.map(doBaz)
ざっくりいうと以下のテクニックで制御構造を減らせる。
- for: map関数, reduce関数
- if: filter関数
- 一時変数、再代入: 関数合成、メソッドチェーン
- nullチェック: Maybeモナド
- 例外ハンドル: Eitherモナド
モナドなんかを使うのはやめた方がいいけど、map, reduce, 関数合成あたりは積極的に使っていってもコンセンサスは得られるだろう。
具体例:誕生日のユーザにクーポンを送付
const isCouponUser = month => user => month === user.birth.month
const createMessage = tmpl => user => yourTemplateEngine.apply(tmpl, user)
const sendEmail = sender => msg => sender.send(msg)
const distributeCoupon = (emailSender, month, tmpl, userList) => {
userList.filter(isCouponUser(month))
.map(createMessage(tmpl))
.map(sendEmail(emailSender))
}
#03. 業務ロジックには名前をつける
たとえ些細なロジックであっても、業務ロジックは関数に切り出して名前をつけよう。そうすることで、このプログラムは何がしたいのかを、関数定義の一覧からある程度読み取ることができる。
以下の例は、数人の期末試験の結果をもとに 科目別の平均点を計算する関数であるが、こんな単純な処理でさえ、かなり読みにくいものになっている。制御構造と業務ロジックがまぜこぜになっているためだ。
const testResult = [
{ user: 'Alice', scores: { math: 30, music: 90 } },
{ user: 'Bob', scores: { math: 90, music: 20 } },
{ user: 'Chales', scores: { math: 70, music: 60 } },
{ user: 'Daniel', scores: { math: 10, music: 20 } },
];
const makeReport = (result) => {
const report = {};
for (const subject of ['math', 'music']) {
let total = 0;
for (const entry of result) {
total += entry.scores[subject];
}
const average = total / result.length;
report[subject] = average;
}
return report;
};
平均点を求める処理を関数にして読みやすい名前を付けて外だしすると、ぐっと読みやすくなる。
const calcAverage = (subject, result) => (
result.reduce((acc, x) => acc + x.scores[subject], 0) / result.length
);
const makeReport2 = result => ({
math: calcAverage('math', result),
music: calcAverage('music', result),
});
#04. 焦点をデータから操作にずらす
以下そのうちにかく