TL;DR
- コードがシンプルになる
- テストが楽チン
- 副作用がない
- モダンJSは関数型推し
-
map、filter、reduceさえ覚えとけば大体イケる
きっかけ
React書いてて改めて気になったので。。
体系的なコト
関数型プログラミングは宣言型プログラミングの一種らしいです。
- 命令型プログラミング
- 「どうやって」(how)に着目する
- 昔のC言語みたいな
- 宣言型プログラミング
- 「何をするか」(what)に着目する
- 最近だとScalaとか
ex) 書いてみる
- 命令型プログラミング
/** 単語を全てハイフンで繋ぐ */
const str = "I have a pen"
let convStr = ""
for (let i = 0; i < str.length; i++) {
if (str[i] === " ") convStr += "-"
else convStr += str[i]
}
console.log(convStr) // I-have-a-pen
- 宣言型プログラミング
const str = "I have a pen"
const strConnectHyphen = (str) => str.replace(/ /g, "-")
const convStr = strConnectHyphen(str)
console.log(convStr) // I-have-a-pen
考察
- 命令型プログラミング
- コメントがないと一見して何をしているか分からない
- 宣言型プログラミング
- 関数名から何をしてるかすぐに分かる(きちんと命名していれば)
なるほど。。
「やること」(what)を宣言的に記述し、
「どうやって」(how)の部分を抽象化することでコードが見やすくなるわけですね。
じゃあ関数型って?
諸説あるようですが、天下のオライリー様によると、、
- データは全てイミュータブル(変更不可)である
- 純粋関数である
むむ、、なんのこっちゃ。
ex) イミュータブルなデータ
- ミュータブルな例
- 元のオブジェクトを変更してしまう(破壊的メソッド)
const theme_green = {
title: "darkgreen",
color: "#006400",
opacity: 0.5
}
const changeOpacity = (color, opacity) => {
color.opacity = opacity
return color
}
console.log(changeOpacity(theme_green, 0.8).opacity) // 0.8
console.log(theme_green.opacity) // 0.8 <- 変わっちゃった
- イミュータブルな例
- 元のオブジェクトは変わらない(非破壊的メソッド)
const theme_green = {
title: "darkgreen",
color: "#006400",
opacity: 0.5
}
const changeOpacity = (color, opacity) => ({...color, opacity})
console.log(changeOpacity(theme_green, 0.8).opacity) // 0.8
console.log(theme_green.opacity) // 0.5 <- 変わらない(イミュータブル)
純粋関数
- 少なくとも一つの引数をとり、値もしくは関数を戻り値として返す
- 引数は、イミュータブルである
- 副作用(関数外への影響)が発生しない
- 引数以外の値を参照しない
- 同じ引数ならば、同じ値が返ってくる
ex) 純粋ではない関数
前述の例です。引数colorが変更されているので純粋関数ではない
const theme_green = {
title: "darkgreen",
color: "#006400",
opacity: 0.5
}
const changeOpacity = (color, opacity) => {
color.opacity = opacity
return color
}
ex) 純粋な関数
同じく前述の例。新たなオブジェクトを生成し返しているので、副作用なし
const theme_green = {
title: "darkgreen",
color: "#006400",
opacity: 0.5
}
const changeOpacity = (color, opacity) => ({...color, opacity})
考察
- 要はスコープは狭く、結合度を下げろってことですね
- 引数さえコントロールすればいいので、テストが簡単そう
具体的にはどうしろと?
思うと、ES6以降のJSって、関数型で書けって言っているような構文ばかりですよね。
個人的によく使うケースをいくつか。。
map、filter、reduceを使い倒す
/** raw data */
const theme_colors = [
{
title: "darkgreen",
color: "#006400",
opacity: 0.5
},
{
title: "tomato",
color: "#ff6347",
opacity: 0.7
},
{
title: "navy",
color: "#000080",
opacity: 0.5
},
]
配列から任意の要素を抽出する/削除する
/** opacity=0.5の要素を抽出 */
const selected = theme_colors.filter(theme => theme.opacity == 0.5)
オブジェクトの配列から任意のプロパティだけ抽出する
/** カラーコードだけを抽出 */
const selected = theme_colors.map(theme => theme.color)
/** 新しいプロパティにして抽出 */
const selected = theme_colors.map(theme => ({code: theme.color}))
配列の任意の要素を変更する
/** 特定のタイトルを変更する */
const editThemeTitle = (before, after, arr) =>
arr.map(item => (item.title == before ? {...item, title: after} : item))
const changed = editThemeTitle("navy", "darkblue", theme_colors)
メソッドチェーンで繋ぐ
/** opacity=0.5の要素のタイトルだけ抽出 */
const selected = theme_colors
.filter(theme => theme.opacity == 0.5)
.map(theme => theme.title)
オブジェクト->配列に変換する
const theme_colors = {
"darkgreen": "#006400",
"tomato": "#ff6347",
"navy": "#000080"
}
const arr = Object.keys(theme_colors).map(key => ({
title: key,
color: theme_colors[key]
}))
配列->単一の値に変換する
/** 最大値を求める */
const numbers = [21, 18, 42, 40, 64, 63, 34]
const maxNumber = numbers.reduce((max, number) => (number > max ? number: max), 0)
配列->オブジェクトに変換する
/** titleをkey, color,opacityをプロパティにもつオブジェクトにする */
const obj_colors = theme_colors.reduce((colors, {title, color, opacity}) => {
colors[title] = {color, opacity}
return colors
}, {})
高階関数を使う
「関数を引数に取るか、戻り値として関数を返す、もしくはその両方」を満たす関数
map、filter、reduceは全て高階関数(関数を引数に取るパターン)
戻り値として関数を返すパターン
const userLogs = userName => message => console.log(`${userName} -> ${message}`)
/** ロギングを開始する */
const log = userLogs("John Doe")
log("logIn")
/** 非同期処理の完了を通知する */
fetchData
.then(_ => log("success to load data.")) // John Doe -> success to load data.
.catch(_ => log("failed to load data.")) // John Doe -> failed to load data.
再帰関数を使う
関数の中から自分自身を再帰的に呼び出す関数
const countDown = (value, fn) => {
fn(value)
return value > 0 ? countDown(value-1, fn) : value
}
countDown(10, value => console.log(value))
まとめ
なんかES6構文の解説みたくなっちゃいました。。
関数型だからこう書く、ということではなく、読みやすいコードを書くことが大事なので
臨機応変にやればいいかなと思います。
多分、純粋関数を制約にしてしまったら、難しい局面とか出てきそう。。
機会があれば、React実践編みたいなのもご紹介できればと思います。