こんにちは。新人プログラマーの岩本です。
今回は「純粋関数」について、自分なりに調べたことをまとめたいと思います。純粋関数自体は関数型言語の概念ですが、普段のコーディングにも活かすことができると思います。
本記事では純粋関数がなぜ重要であり、その特徴や利点について探っていきます。
ぜひ最後までご覧ください。
純粋関数とは?
純粋関数とは以下の条件を満たす関数のことです。
- 返り値が常に1つであること
- 引数にのみ基づいて、返り値を計算すること
- 既存の値を変更しない
シンプルな定義ですが、これを意識するだけでコードがより堅牢になり保守しやすくなります。
それぞれ詳しく見ていきましょう。
1. 返り値が常に1つであること
純粋関数の1つ目の条件は、返り値が常に1つであることです。言い換えると、純粋関数は常に1つの値を返します。
以下の2つの関数を比べてみましょう。
// 非純粋関数
func increment( _ a: inout Int) { // 参照渡し。返り値を返さない
a += 1
}
// 純粋関数
func increment(_ a: Int) -> Int { // 返り値を1つ返す
return a + 1
}
この2つの関数の処理は全く同じことをしています。渡された値に1を足しています。
しかし一方の関数は引数として参照を渡し、返り値を返しません。それにより関数内に副作用が発生し、コードの予測が難しくなってしまいます。
不用意に副作用を発生させると、意図せぬところでバグが発生し、原因の特定が困難になってしまいます。
一方返り値を返す関数では、副作用は発生していません。これによりコードの保守性が高まり、意図しないバグの発生をおさせることができます。
2. 引数にのみ基づいて、返り値を計算すること
純粋関数の2つ目の条件は、引数だけを使って返り値を計算していることです。言い換えると関数内では引数しか使わないということです。
以下の2つの関数を比べてみましょう。
var globalValue = 3
// 非純粋関数
func calculateMultiplication(_ a: Int) -> Int { // 引数以外の値を使用
return a * globalValue
}
// 純粋関数
func calculateMultiplication(_ a: Int, _ b: Int) -> Int { // 引数のみを使用
return a * b
}
引数のみに基づいて計算する関数は、参照透過性を持っています。参照透過性とは同じ引数であれば、同じ答えが返ってくることです。
引数が同じであれば常に同じ答えが返ってくるので、余計なバグを心配する必要はありません。
一方引数以外の値(この場合globalValue)を使っている関数は、同じ引数を渡しても同じ答えが返ってくる保証はありません。globalValueが変更されれば、この関数を使っている箇所でバグが発生する可能性があります。
3. 既存の値を変更しない
純粋関数の3つ目の条件は、既存の値を変更しないことです。
以下の関数を見てください。
var sum = 0
let values = [1, 2, 3, 4, 5, 6, 7, 8, 9]
// 非純粋関数
func sumArray(_ array: [Int]) {
for i in values {
sum += i
}
}
sumArray(values)
print(sum) // 45
sumArray関数は受け取った配列の合計値をsumに格納する関数です。
一見正しく動作していますが、大きな問題があります。sumの値によっては意図しない動作をする可能性があります。
たとえばsumArray関数を2回呼び出してみましょう。
sumArray(values)
sumArray(values)
print(sum) // 90
返ってきてほしい値は45なのに、90となってしまいました。正しく動作させるためには、毎回sumを0にする必要があります。
ではこのsumArray関数を純粋関数に書き換えてみましょう。
let values = [1, 2, 3, 4, 5, 6, 7, 8, 9]
// 純粋関数
func sumArray(_ array: [Int]) -> Int {
values.reduce(0) { sum, value in sum + value }
}
print(sumArray(values)) // 45
sum変数を削除し、sumArray内での既存の値を変更する処理を無くしました。
こうすることで何度関数を呼び出しても、引数が同じであれば常に同じ答えが返ってきます。
純粋関数のメリット
ここまで純粋関数の条件について解説してきましたが、ここからはそのメリットについて解説します。
純粋関数のメリットは大きく以下の2つです。
- 保守しやすくなる
- テストしやすくなる
それぞれ解説します。
保守しやすくなる
純粋関数のメリットの1つ目は、コードが保守しやすくなるということです。
関数の処理内容はシグネチャ(名前や引数の型、返り値などの組み合わせのこと)を見れば理解できますし、副作用もないので意図しないバグの発生を防ぐことができます。
たとえば指定した商品をカートに入れる関数を考えてみましょう。
以下は非純粋関数の例です。
var cart: [商品] = [...]
// 非純粋関数
func addItem(item: 商品) {
if (カードの中身がいっぱい) {
return
}
cart.append(item)
}
この関数ではカートの状態によって商品を入れるか入れないかを判別しています。そのせいで、関数を使う側は処理内容をしっかりと読んだ上で使う必要が生じ、保守しにくいコードになってしまいます。
また引数以外の変数を値を使っているので、参照透過性が失われています。
ではこの関数を純粋関数にしてみましょう。
// 純粋関数
func addItem(item: 商品, cart: [商品]) -> [商品] {
return cart + [item]
}
この関数は処理内容を見ずとも、どんなことをするのかが分かり易いと思います。また既存の値を変更していないので、バグが発生することもありません。
以上から関数を純粋にすると、読みやすく理解しやすい、保守性の高いコードになると言えます。
テストしやすくなる
ここまで読んでいれば、純粋関数がテストをしやすいのは理解できるはずです。
純粋関数には参照透過性があるので、入力に対しての出力をテストすれば、関数が正しいかどうかがわかります。
テストが嫌いなエンジニアは純粋関数を心がければ、テストの負担を減らすことができます。
まとめ
今回は純粋関数について解説しました。
もちろん全ての関数を純粋関数にするのは難しいですが、意識するだけでもより良いコードになると思います。
またこのように関数型言語の概念で、普段のコーディングの役に立つ知識はまだまだたくさんあるので、今後も定期的に紹介していこうと思います。
ここまでのご閲覧ありがとうございました!
コメント