【関数型】新人エンジニアが教える「純粋関数」

設計パターン

こんにちは。新人プログラマーの岩本です。

今回は「純粋関数」について、自分なりに調べたことをまとめたいと思います。純粋関数自体は関数型言語の概念ですが、普段のコーディングにも活かすことができると思います。

本記事では純粋関数がなぜ重要であり、その特徴や利点について探っていきます。

ぜひ最後までご覧ください。

この記事は新卒エンジニアが執筆しています。
そのため内容に間違いや不備がある場合があります。
もし間違いを発見しましたら、どんどん指摘していただけると幸いです。

純粋関数とは?

純粋関数とは以下の条件を満たす関数のことです。

  1. 返り値が常に1つであること
  2. 引数にのみ基づいて、返り値を計算すること
  3. 既存の値を変更しない

シンプルな定義ですが、これを意識するだけでコードがより堅牢になり保守しやすくなります。

それぞれ詳しく見ていきましょう。

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. 保守しやすくなる
  2. テストしやすくなる

それぞれ解説します。

保守しやすくなる

純粋関数のメリットの1つ目は、コードが保守しやすくなるということです。

関数の処理内容はシグネチャ(名前や引数の型、返り値などの組み合わせのこと)を見れば理解できますし、副作用もないので意図しないバグの発生を防ぐことができます。

たとえば指定した商品をカートに入れる関数を考えてみましょう。

以下は非純粋関数の例です。

var cart: [商品] = [...]
// 非純粋関数
func addItem(item: 商品) {
    if (カードの中身がいっぱい) {
        return
    }
    cart.append(item)
}

この関数ではカートの状態によって商品を入れるか入れないかを判別しています。そのせいで、関数を使う側は処理内容をしっかりと読んだ上で使う必要が生じ、保守しにくいコードになってしまいます。

また引数以外の変数を値を使っているので、参照透過性が失われています。

ではこの関数を非純粋関数にしてみましょう。

// 純粋関数
func addItem(item: 商品, cart: [商品]) -> [商品] {
    return cart + [item]
}

この関数は処理内容を見ずとも、どんなことをするのかが分かり易いと思います。また既存の値を変更していないので、バグが発生することもありません。

以上から関数を純粋にすると、読みやすく理解しやすい、保守性の高いコードになると言えます。

テストしやすくなる

ここまで読んでいれば、純粋関数がテストをしやすいのは理解できるはずです。

純粋関数には参照透過性があるので、入力に対しての出力をテストすれば、関数が正しいかどうかがわかります。

テストが嫌いなエンジニアは純粋関数を心がければ、テストの負担を減らすことができます。

まとめ

今回は純粋関数について解説しました。

もちろん全ての関数を純粋関数にするのは難しいですが、意識するだけでもより良いコードになると思います。

またこのように関数型言語の概念で、普段のコーディングの役に立つ知識はまだまだたくさんあるので、今後も定期的に紹介していこうと思います。

ここまでのご閲覧ありがとうございました!

参考にした書籍・記事

純粋関数とは
純粋関数 - Qiita
純粋関数について説明する。純粋関数の特徴は以下である。引数が同じ場合、常に同じ返り値となる。(参照透過性)副作用が発生しない例えば、下記のようなadd関数は純粋関数である。function…

コメント

タイトルとURLをコピーしました