「イミュータブル」って何?知らないと怖い副作用を防ぐプログラミングの知恵

こんにちは。ゆうせいです。

関数にデータを渡して処理をお願いしたら、いつの間にか元のデータの中身まで書き換えられていて、バグの原因探しに何時間も溶かしてしまった…。

新人エンジニアの頃、そんな苦い経験をしたことはありませんか?

実はこの問題、「副作用」という現象が原因であることが多いのです。

そして、この厄介な副作用からあなたのコードを守ってくれる、強力な考え方が存在します。それが今回のテーマである「イミュータブル(Immutable)」です。

なんだか難しそうな言葉に聞こえるかもしれませんが、大丈夫!一緒にゆっくり見ていきましょう。


プログラミングにおける「副作用」とは?

まず、問題の中心にいる「副作用」という言葉から解説しますね。

お薬の話ではありませんよ!プログラミングの世界でいう副作用とは、「ある関数が、その関数の外にあるデータ(状態)を、知らぬ間に変更してしまうこと」を指します。

例えるなら、ジュースを買うための自動販売機を想像してみてください。

お金を入れてボタンを押すと、ガコンとジュースが出てきますよね。これは期待通りの「主作用」です。

もし、この自動販売機がジュースを出すのと同時に、あなたが全く知らないうちに、隣のコーヒーの値段を10円値上げしていたらどうでしょう?

「え、そんなこと頼んでないのに!」って思いますよね。この、頼んでもいない余計な変更が「副作用」です。

プログラムでも同じことが起こります。ある関数が、計算結果を返すだけでなく、引数として受け取った元のデータの中身を勝手に書き換えてしまう。これが、追跡しづらいバグの温床になるのです!


主役の登場!「イミュータブル(Immutable)」

では、どうすれば副作用から身を守れるのでしょうか?ここで主役の登場です。

イミュータブル(Immutable)とは、英語で「im-(〜できない)」と「mutable(変更可能な)」を組み合わせた言葉で、その名の通り「変更不可能」という意味を持ちます。

一度作成したら、その中身を一切変えることができないデータやオブジェクトのこと、それがイミュータブルです。

これも例え話で考えてみましょう。

  • 変更可能な(ミュータブルな)データ: ホワイトボードのようなものです。誰でも自由に書いたり消したり、内容をどんどん変更できます。
  • 変更不可能な(イミュータブルな)データ: 一度現像した写真のようなものです。写真に写っている人物を後から消したり、別の背景に変えたりすることはできませんよね。もし変更を加えたいなら、元の写真とは別に、新しい写真を作り直すしかありません。

Pythonでは、リストは変更可能(ミュータブル)、タプルは変更不可能(イミュータブル)なデータの代表例です。

# リストは変更可能(ミュータブル)
mutable_list = [1, 2, 3]
print(f"変更前: {mutable_list}")

# 中身を直接変更できる
mutable_list.append(4)
print(f"変更後: {mutable_list}")


# タプルは変更不可能(イミュータブル)
immutable_tuple = (1, 2, 3)
print(f"元のタプル: {immutable_tuple}")

# 変更しようとするとエラーになる!
# immutable_tuple.append(4) # これはエラー

# 変更したい場合は「新しいタプル」を作る
new_tuple = immutable_tuple + (4,)
print(f"新しいタプル: {new_tuple}")
print(f"元のタプルは変わらない: {immutable_tuple}")

コードを見ると、タプルは元のデータが一切変わっていないのがわかりますね。


なぜイミュータブルだと副作用を防げるの?

ここまで来れば、もう答えは目前です。

もし、関数に渡すデータが「変更不可能な(イミュータブルな)写真」だったらどうなるでしょうか?

その関数は、受け取った写真をどう頑張っても変更することはできません。つまり、関数の外にある元のデータが、勝手に書き換えられる心配が一切なくなるのです!

先ほどの副作用が起きてしまったコードを考えてみましょう。

# 副作用を起こす可能性がある関数
def process_data(items):
  # itemsの中身を書き換えてしまう
  items.append('processed')
  print("関数内部でリストを変更しました")

# 変更可能なリストを渡す
my_list = ['data1', 'data2']
print(f"関数を呼ぶ前のリスト: {my_list}")

process_data(my_list)

print(f"関数を呼んだ後のリスト: {my_list}")
# 出力結果: 関数を呼んだ後のリスト: ['data1', 'data2', 'processed']
# → 元のデータが書き変わってしまった!

もし my_list がタプルのようなイミュータブルなデータであれば、 process_data 関数の中で append しようとした瞬間にエラーが発生します。これにより、意図しないデータの書き換えを未然に防ぐことができるのです。

データをイミュータブルに保つということは、「このデータは絶対に変わらない」という安心感を手に入れることであり、プログラム全体の予測可能性を劇的に高めてくれる素晴らしい工夫なのです。


まとめと今後の学習

今回は、イミュータブルという考え方が、なぜ副作用を防ぎ、安全なコードに繋がるのかを解説しました。

  • 副作用: 関数が、その外側にあるデータを意図せず変更してしまうこと。
  • イミュータブル: 一度作ったら変更できないデータのこと。
  • 効果: イミュータブルなデータを扱うことで、関数による元のデータの書き換えを防ぎ、副作用のない安全なプログラムを書きやすくなる。

もちろん、何でもかんでもイミュータブルにすれば良いというわけではありません。頻繁にデータを変更する場合、毎回新しいオブジェクトを作るとパフォーマンスが落ちる可能性もあります。

ですが、まずはこの「イミュータブル」という考え方をしっかり身につけてみてください。そして、データの不必要な変更を防ぐことが、どれだけプログラムのデバッグを楽にし、コードの品質を高めるかに気づくはずです。

次の一歩として、副作用を全く持たない「純粋関数」という概念について学んでみると、今日の話がさらに深く理解できるのでおすすめですよ!

投稿者プロフィール

山崎講師
山崎講師代表取締役
セイ・コンサルティング・グループ株式会社代表取締役。
岐阜県出身。
2000年創業、2004年会社設立。
IT企業向け人材育成研修歴業界歴20年以上。
すべての無駄を省いた費用対効果の高い「筋肉質」な研修を提供します!
この記事に間違い等ありましたらぜひお知らせください。