関数の合成
- 関数型プログラミングではロジックを分割して、小さな関数として定義する
- 関数は純粋関数であること(以下の特性を持つ)
- 引数を取り、戻り値を返す
- 引数はイミュータブル
- 副作用は発生させない
- それらの関数を合成して、ロジックを完成させる。
実装例(ひらがなと半角英数への文字列変換)
- 引数の文字列をひらがなと半角英数に変換する
- 検索文字列と検索対象の文字列を統一してフィルタ検索するのに使ったりする。
- 処理の流れは以下。各処理は前処理の戻り値を引数にし、変換処理を行い、次の処理へ結果の文字列を渡す。
- IN:対象文字列
- 1.大文字変換→2.全角カナ変換→3.ひらがな変換→4.半角(英数)変換
- OUT:変換後の文字列
- ※変換処理の詳細については、今回の記事の趣旨でないため割愛(ちなみに、この順序で変換する必要がある)
関数合成を利用しない場合(駄目な例)
- 関数の合成を使用しない例
- 関数が入れ子になっており、処理を追いづらい
- 関数の追加や削除があった際に改修が面倒
// 2.全角カナ変換
function halfKanaToFullKana(str: string) {
// 半角カナ→全角カナ変換は、変換表で愚直に置き換える必要がある。(KanaMap)
const regExp = new RegExp(`(${Object.keys(KanaMap).join('|')})`, 'g');
return str
.replace(regExp, function (match) {
const key = match as keyof typeof KanaMap;
return KanaMap[key];
})
.replace(/゙/g, '゛')
.replace(/゚/g, '゜');
}
// 3.ひらがな変換
function kanatoHira(str: string) {
return str.replace(/[\u30a1-\u30f6]/g, function (s) {
return String.fromCharCode(s.charCodeAt(0) - 0x60);
});
}
// 4.半角(英数)変換
function fullToHalf(str: string) {
return str.replace(/[!-~]/g, function (s) {
return String.fromCharCode(s.charCodeAt(0) - 0xfee0);
});
}
// 1.大文字変換→2.全角カナ変換→3.ひらがな変換→4.半角(英数)変換
// 再帰処理のため、内側の関数から外側に向かって処理が実行されている。
// 修正も困難であるし、関数が増えたらすごく読みづらい!!
export function toSearchWord(str: string) {
return fullToHalf(kanatoHira(halfKanaToFullKana(str.toUpperCase())));
}
// 実行
const word = searchWordFunction('あアaAアA');
console.log(word); // ああAAあA
関数合成を利用する場合(良い例)
- 関数を持ち回すため変数に格納する(関数第一級オブジェクトの特性)
- 高階関数とreducerによる再帰処理を行う関数を用意する(compose関数)
- compse関数の引数に、合成する関数の配列を指定することで、関数合成によるロジックの完成
// 1.大文字変換
// 関数合成で使用するため、組み込み関数をラップして変数で持ち回す。
const covertUpperCase = (str: string) => str.toUpperCase();
// 2.全角カナ変換
const halfKanaToFullKana = (str: string) => {
const regExp = new RegExp(`(${Object.keys(KanaMap).join('|')})`, 'g');
return str
.replace(regExp, function (match) {
const key = match as keyof typeof KanaMap;
return KanaMap[key];
})
.replace(/゙/g, '゛')
.replace(/゚/g, '゜');
};
// 3.ひらがな変換
const kanaToHira = (str: string) =>
str.replace(/[\u30a1-\u30f6]/g, (s) =>
String.fromCharCode(s.charCodeAt(0) - 0x60)
);
// 4.半角(英数)変換
const fullToHalf = (str: string) =>
str.replace(/[!-~]/g, (s) => String.fromCharCode(s.charCodeAt(0) - 0xfee0));
// 関数配列を引数に、関数を返却する(高階関数)
// 返却する関数は、reducerで前の結果を受け取り(argとcompose)、
// 関数を再帰的に実施する(f(compose))
const compose =
(...fns: Function[]) =>
(arg: string) =>
fns.reduce((compose, f) => f(compose), arg);
// 1.大文字変換→2.全角カナ変換→3.ひらがな変換→4.半角(英数)変換
// 処理の順番通りに関数を渡してやればよいので直感的
// 関数の追加や削除も楽。
export const searchWord = (str: string) =>
compose(
covertUpperCase,
halfKanaToFullKana,
kanaToHira,
fullToHalf)
(str);
// 実行
const word = searchWord('あアaAアA');
console.log(word); // ああAAあA
終わりに
- 関数型プログラミングになれるまでは、駄目な例の書き方を行っていた。
- 高階関数や、純粋関数、カリー化、関数の第一級オブジェクト等、関数型プログラミングの特性を理解し、関数合成を駆使したプログラミングを意識したい。
- 今回のtipsでは、上記を踏まえ、compose関数が全てといってよい。