前提知識
- 副作用とは?
「副作用」という言葉はプログラミングの文脈で広く使われていますが、共通の定義があります。それは、「関数が外部の状態を変更したり、外部の状態に依存することで、関数の出力が予測不能になること」です。
✅ 副作用がある関数
- 外部状態の変更
関数が外部の状態を変更する場合(例: グローバル変数やDOMの状態、APIの呼び出し結果を変更する)。 - 予測不可能な結果
関数が外部の状態に依存しており、同じ入力に対して常に同じ結果を返さない場合(例: 外部APIからのデータに依存する)。
✅ 副作用がない関数
関数の出力が入力だけに依存し、関数が実行されても外部の状態を変更しない関数。これは「純粋関数」と呼ばれる。同じ入力に対して常に同じ結果を返し、関数の実行がプログラムの他の部分に影響を与えない。
💬具体的にどんなものがあるの??
- DOMの変更
DOMの操作は外部状態に影響を与えるため、一般的には副作用とされます。ただし、初期レンダリングは変更ではなく描画処理なので副作用とはみなされません。 - APIとの通信
API呼び出しは外部システムの状態に依存して結果が変わるため、引数以外の要因で戻り値が変わる可能性があるため、副作用とみなされます。 - ファイルへの書き込み
これはプログラムの外部(ファイルシステム)に影響を与えるため、典型的な副作用です。 - state/propsの変更
Reactにおいて、useStateやuseEffectを使った操作は、レンダリングサイクルに影響を与えるため、副作用とみなされます。
例)useEffect内でAPI呼び出しを行い、その結果をuseStateで管理する場合、この操作は副作用です。なぜなら、これが行われるとReactは再レンダリングをトリガーし、UIが変わるからです - オブジェクトや配列の変更
関数内でオブジェクトや配列を変更すると、その変更が関数外にも影響を及ぼす可能性があります(たとえば、参照型データの変更)。この場合も副作用とされます。
なるべく副作用のない関数を作る(本題)
この議題については、賛否や程度の問題があると思うのですが、ここでは基本的な考え方として「副作用のない関数を作ることに努めるべきである」という立場に身を置きます。
なぜ副作用のない関数を作った方がいいの??
副作用を避ける理由は、コードの予測可能性と保守性を高めるためです。
-
予測可能性
副作用のない関数は、同じ入力に対して常に同じ結果を返すため、テストやデバッグが容易です -
保守性
副作用のない関数は、他のコードに影響を与えにくいため、コードの変更が他の部分に予期せぬ影響を与えるリスクが低くなります。これにより、コードの保守が容易になります。
でもAPIコール(副作用)とか絶対に必要だし、どうするべき?
APIコールやファイルへの書き込みなど、副作用が避けられない状況は多々あります。その場合でも、以下の方法を用いて副作用を管理できます。
-
副作用を隔離する
副作用を持つコードは、できるだけ限定された場所に隔離し、他の純粋な関数から分離します。たとえば、ReactのuseEffectフックを使って、副作用をコンポーネントのライフサイクルに沿って管理します。 -
関数の戻り値に副作用を持たせない
副作用を引き起こす関数でも、戻り値は副作用の影響を受けないようにします。例えば、APIコールの関数は、その結果を呼び出し元に返すだけで、呼び出し元でその結果を使って状態を更新します。 -
明示的な副作用管理
ReduxやContext APIを使い、アプリケーションの状態管理を明確に行います。これにより、副作用がどこで起きるかが分かりやすくなります。
おまけ
副作用のある関数と戻り値の関係
-
戻り値がない関数
通常、必ず副作用を持つ関数です。何かを実行する(例: DOMの更新、API呼び出し etc)ために使われます。 -
戻り値がある関数
基本的には副作用ではない状態が望ましい。しかし100%戻り値がない = 純粋関数とは限りません。
副作用を持つ関数でも、結果を返すことはもちろんできます。ただし、その関数が外部状態を変更しない、あるいは外部状態に依存しないならば、純粋関数とみなされます。