プログラミング入門 純粋関数とは何かを知っておくとコードをシンプルにかけるようになる話

JavaScriptのような関数型言語ででてくる純粋関数について書きます。

純粋関数とは

純粋関数とは、入力に対していつも同じ出力を返す関数のことです。また、副作用がない関数でもあります。

これだとなんのことか分からないので、例をみてみましょう。

まず、純粋関数です。

const greet = (name, word) => {
  return word + '!!' + name
}

console.log(greet('tamibouz', 'Hello')) // Hello!!tamibouz

nameとwordという入力(引数)を受け取り、それらを元に出力を決定しています。これは入力が変わらなければ出力も変わりません。

続いて純粋関数ではない関数です。

let word = 'Hello'

const greet = (name) => {
  return word + '!!' + name
}

console.log(greet('tamibouz')) // Hello!!tamibouz

こちらも「Hello!!tamibouz」を返します。

しかし、word という変数が greet 関数の外にあり、それを参照しているという違いがあります。

もし、このwordという変数の中身が書き換えられたら、出力結果が変わってしまいます。

let word = 'Hello'

const greet = (name) => {
  return word + '!!' + name
}

console.log(greet('tamibouz')) // Hello!!tamibouz
word = 'Bye'
console.log(greet('tamibouz')) // Bye!!tamibouz

純粋関数とは入力に対していつも同じ出力を返す関数と書きましたが、上記では’tamibouz’という入力(引数)に対して一回目と2回目で異なる結果を返しているため、純粋関数ではないと言えます。

また、関数の外の状態(今回で言うとwordという変数)に影響されるため副作用があります

副作用についてもう少し詳しく

副作用について別の例をみてみましょう。

const arr = [1,2,3]

const addElement = (element, arr) => {
  arr.push(element)
}

addElement(4, arr)
console.log(arr) //[1,2,3,4]

addElementは配列に要素を追加する関数です。さきほどの関数のように、関数の外のarrを直接見ておらず、引数で渡しています。これなら純粋関数と言えるでしょうか。

これも純粋関数とは言えません。

なぜかというと、arr の中身を書き換えてしまっているからです。純粋関数は出力を返すだけが仕事ですが、このコードでは arr の中身を変えてしまっているので副作用がある、といえます。

純粋関数にすると何がうれしいか

純粋関数はテストがしやすい

テストとはコードが想定通りに動いているかをチェックする作業です。一つ目の例で言うと、greet関数に’tamibouz’と’Hello’を渡すと’Hello!!tamibouz’が返るかどうかをチェックします。

純粋関数であれば、入力に対して出力が正しいかをみるだけです。

しかし、純粋関数でない場合は、さきほどの例のように関数の外の状態がどうなっているかを考慮する必要があります。

今回の例では、一つの外の状態に依存していますが、何十もの状態に依存してくると、テストする際にはそれらの依存を毎回考慮する必要がでてきてしまい、非常に大変です。

予期せぬバグを見つけやすい

純粋関数ではない場合、出力を返す以外にどこかに影響を与えてしまっているので、バグを発見しづらくなります。

以下は実際に実務であった例を簡略化したものです。

以下のような配列をサーバー側から受け取り、配列の順序のまま画面に一覧を表示していました。

const items = [
  { name: 'orange', price: 210 },
  { name: 'apple', price: 370 },
  { name: 'Strawberry', price: 500 },
  { name: 'grape', price: 450 },
  { name: 'banana', price: 120 },
]

しかし、ある時から突然順序が変わってしまいました。

表示順序を変えるような処理は誰も入れておらず、何が原因で勝手に順序が変わったか調査が始まりました。

調査の結果、最近以下のような関数が実装されており、その関数が副作用を持っていたことが原因であることが分かりました。

const getMinPrice = (list) => {
  const sorted = list.sort(function (a, b) {
    return a.price - b.price
  })

  return sorted[0]
}

この関数は最も安い値段を取得するものです。受け取った list を値段が安い順にソートし、ソートした後の0番目の要素を返しています。

処理自体は正しいのですが、このソート処理によって元の配列の順序が変わってしまっていました。

const items = [
  { name: 'orange', price: 210 },
  { name: 'apple', price: 370 },
  { name: 'Strawberry', price: 500 },
  { name: 'grape', price: 450 },
  { name: 'banana', price: 120 },
]

const getMinPrice = (list) => {
  const sorted = list.sort(function (a, b) {
    return a.price - b.price
  })

  return sorted[0]
}

const min = getMinPrice(items)
console.log(min) // { name: 'banana', price: 120 } を返しているのはこの関数としては正しいが。。。

console.log(items) 
// getMinPrice 内のソート処理によって、元のitemsの順序が変わってしまっていた
// [ { name: 'banana', price: 120 },
  // { name: 'orange', price: 210 },
  // { name: 'apple', price: 370 },
  // { name: 'grape', price: 450 },
  // { name: 'Strawberry', price: 500 } ]

このように本来の関数の仕事以外でどこかに影響を与えてしまっていると、バグを見つけづらくなり、調査に時間がかかってしまいます。

まとめ

今回は純粋関数について紹介しました。

システムを実装する上で、すべての関数を純粋関数にするのは困難だと思います。

しかし、純粋関数にできるものはできるだけするように心掛け、副作用のある処理と明確にわけることで、きれいなコードになり、保守性も上がると思います。