この記事について
筆者は開発する際の言語としてScalaを触っていることが多いのですが、Scalaではvalを使って変数定義をすると再代入を禁止できるため、極力valを使うことが推奨されています
再代入しないという良さについては「リーダブルコード」でも紹介されており(後ほど紹介しますが)、Scalaで体感してからはScalaに限らず積極的に再代入しないような実装を心がけるようになりました
今回は再代入しないことの良さについて説明することで今後この記事を読んで「ちょっと試してみようかな...」と言う人が出てくると嬉しいです
もちろん「いやいやめちゃくちゃ書きづらい」「意識しなくて絶対良い!」と言う人もいるかと思います、この辺りの考え方は人によるとは思いますので「こういう理由で自分はやらない」と言う結論を持つだけでも意味はあるかと思いますので知見の一つとして学んでいただければと思います
筆者が考えている「再代入しない」と言う意味と「不変(immutable)」と言う言葉では意味が違います、詳しくは後述しますが「不変」を追求すると難しく考えてしまう傾向があるため、筆者がまず試してほしいと考えている内容は**「できるところから再代入しないように実装していく」**と言うことなのでそれを前提に読み進めてもらえればと思います
対象読者について
プログラミングを書く人全員
この記事はプログラミング言語は問いません
例えばScalaではvalを使うことで再代入を禁止する(正確には不変にする)ことができますが、C++ではconst、Javaではfinalなど言語に応じて読み変えてもらえればと思います
最悪そういった再代入を禁止するための仕組みが言語として用意されていなくても意識としてそういう実装にするだけでも意味があるのかなとは思っています
サンプルではJavaScriptで記述します
再代入しないというのはどういうことか
JavaScriptの場合letを使って変数定義したものは再代入が可能です
let str = 'hoge'
str = 'huge'
console.log(str) // 出力結果: huge
constを使うことで再代入ができないようにできます
const str = 'hoge'
str = 'huge' // エラーが出る
Uncaught TypeError: Assignment to constant variable.
冒頭に「不変(immutable)」という言葉を使いましたが、これはオブジェクトの状態が変わらないことを指します
JavaScriptのconstは厳密には不変ではありません、例えばconstにオブジェクトを入れるとオブジェクトが指し示す要素については変更可能となってしまうからです
// JavaScriptのconstの中身を書き換えてしまう例
const a = { name: "hoge" }
a.name = "huge"
console.log(a.name) // 出力結果: huge
また破壊的メソッドを使うことでも内容が変わってしまいます
const list = [1, 5, 4, 2, 3]
console.log(list) // 出力結果: [1, 5, 4, 2, 3]
list.sort()
console.log(list) // 出力結果: [1, 2, 3, 4, 5]
筆者としては完璧な不変(immutable)を目指すつもりはなく、あくまでも実装の意識としてJavaScriptだったら極力constにするというのが一旦は良いのかなと考えていますのでサンプルではconstを使っていっています
再代入しない良さ
リーダブルコードより
第2部 9.3 「変数は一度だけ書き込む」より要約
- 変数のスコープが長いとコードが追いかけにくいが、変数が何度も値が変更されると値を追いかける難易度がさらに難しくなってしまう
- この問題と戦うために「変数は一度だけ書き込む」ということを提案したい
- 永続的に変更されない変数は扱いやすい
- 実際多くの言語(Python, Jvaなど)ではStringなどの組み込み型はimmutableになっている
- Javaの作者であるジェームズ・ゴスリングさん曰く「(immutableは)トラブルになる傾向が少ない」だそうだ
- 変数を一度だけ書き込む手法が使えないとしても、変数の変更箇所はできるだけ少なくした方が良い
この記事で伝えたいことが凝縮されているような気がしますが(リーダブルコード凄い)、上記のようなことが書かれていました
また「鍵となる考え:変数を操作する場所が増えると、現在地の判断が難しくなる」とも書かれていて言い得て妙だなと感じました
リーダブルコードに書かれている時点で納得感のある話になっていますがこの次ではもう少し良さを体感できるサンプルを見せようかと思います(数行のサンプルソースコードで良さを見せるのは至難の技なので伝わらないかもしれないですが...)
値を追いやすい
以下の例があったとします
let name = "hoge"
... 間に数千行の色々な処理がある ...
console.log(name)
これの出力結果はわかりますか?
もちろん省略されてしまっているのでわかりませんよね?
ちゃんと実装が書いてあったとしても、もし途中で書き換えが発生していたとすると値を調べるにはもはやデバッグログの出力結果を見る感じになってくるかと思います(簡単に出力結果を見れるならば楽ですが、そうではない環境も世の中にも多いとは思いますので、そうなってくるとひたすら読んでいくしかないですね...!)
もしこれが再代入をしない実装になっていた場合
const name = "hoge"
... 間に数千行の色々な処理がある ...
console.log(name)
これだといかがでしょう?
間の処理を見なくても何が出力されるか一目瞭然ですね!(ちょっと極端な例かもしれませんが)
これが1つの変数だけだと体感しづらいかもですが、何十もの変数があったとして、それらが全て再代入されないと言う方針だった場合に、値が書き換わることがないと言う安心感の元コードが追いやすくなるはずです
値が変わらないということはプログラムを書いていく上で想定外の変更が起きにくくなりバグを抑制することができます、Scalaのvalだと完全な不変であり破壊的メソッド等もないため、プログラム上でどの値がどのように変更されていくのかが追いやすくなります
途中経過がわかりやすい
以下の出力結果がうまくいかなかったとします
let result = 0
result = functionA(result)
result = functionB(result)
result = functionC(result)
console.log(result)
どう調査するかというと関数の後でデバッグログを出す感じになるかと思います
let result = 0
result = functionA(result)
console.log(result) // ここまではOK
result = functionB(result)
console.log(result) // ここまではOK
result = functionC(result)
console.log(result) // ここで意図していないからfunctionCに問題がありそう!!
面倒なのとデバッグログの消し忘れに気をつけなくなりますね...
再代入しない実装にする場合は都度変数を作る形となり、最後の方で1行デバッグログを書くだけで済むため楽チン!
const init = 0
const resultA = functionA(init)
const resultB = functionB(resultA)
const resultC = functionC(resultB)
console.log(`debug: ${resultA}, ${resultB}, ${resultC}`) // resultCの値がおかしいからfunctionCに問題がありそう!!
console.log(resultC)
また都度変数を作成することになるので、それがどんな値が入っているかをちゃんと変数名として表せられればより見やすいコードになるかと思います
const init = 0
const sortedItem = functionA(init) // ソート済みの結果が入っていそうだ
const apiRequestedItem = functionB(sortedItem) // APIリクエストしていそうだ
const fixedItem = functionC(apiRequestedItem) // APIリクエスト結果をゴニョってしていそうだ
console.log(fixedItem)
(上記の例だと関数名なんとかしろよ or コメントつけろよと言うツッコミがあるかもですが)
再代入しない実装について
letを禁止にする場合
もし思い切ってこの方針でやって見ると実装がごろっと変わります
例えばif文で分岐させた結果を取得する場合は
let result = null
if (flagA) {
...
result = XXX
} else {
...
result = YYY
}
console.log(result)
関数切り出しなど考える必要があります
const result = flagA ? functionA() : functionB()
console.log(result)
上記例だとifの分岐の中身をそれぞれ関数化していますが、ひとまとめで切り出すor即時関数化するなど関数化のやり方についても色々ありそうです(詳しくは後述)
Scalaだとifは式なので以下のように書けるのですが...
// Scalaではifの結果を取得できる
val result = if(flagA) {
...
XXX // returnは省略できる
} else {
...
YYY
}
配列を処理させたい場合、簡単に実装したい場合はforなどでやりがちですが
// list1の内容を2倍にしてその合計を取得する
const list1 = [1, 2, 3]
let total = 0
for (let i = 0; i < list1.length; i++) {
total = total + (list1[i] * 2)
}
console.log(list1) // 出力結果: [1, 2, 3]
console.log(total) // 出力結果: 12
JavaScriptの場合はmapを使うと要素を加工した上で新しい配列を作成することができます
const list1 = [1, 2, 3]
const total = list1
.map((item) => item * 2) // 新しく2倍にした配列を返す
.reduce((a, x) => a + x) // 要素毎に計算していくことで最終的に合計値を取得
console.log(list1) // 出力結果: [1, 2, 3]
console.log(total) // 出力結果: 12
また配列操作ではなく単純にループ処理をしたい場合は
let result = 0
for (let i = 1; i <= 5; i++) {
result += i
}
console.log(result) // 15
再帰関数に変更する必要があります
function calc(loopNum) {
if (loopNum <= 0) return 0
return loopNum + calc(loopNum - 1)
}
console.log(calc(5)) // 15
forで慣れている人にとって急に再帰で書き換えるのはハードルが高いと思っています、また読みやすさの観点からもforで書いた方が読みやすいかと思うため、let禁止など思い切った方針にすることは実装がごろっと変わってしまうため要検討かと思います
なので方針については「再帰を使わなければならなくなりそうなところだけletを使う」または「基本はconst、letを使わないと処理が長くなってしまう場合はletを使う」などのある程度の緩さを残した方針にするのが良いのかなと思っています
それでもif文の書き方や配列操作についてはやはり実装が変わってしまうため「そこまでして再代入しない方針にしたくない...」という人はこの次に紹介するテクニックを試してもらえればと思います
処理後にconst化させる
※ ↓は @standard-software さんからのコメントを元に追記したテクニックですmm ありがとうございますmm
変数をlet _XXX
などで用意しておき終わったらconst XXX = _XXX
とconst化するような方針にしていくと後続は再代入されることはなくなり、また_がつけものだけ可変変数だなど見分けもつくので良さそうです!
const list1 = [1, 2, 3]
let _total = 0
for (let i = 0; i < list1.length; i++) {
_total = _total + (list1[i] * 2)
}
const total = _total // const化させる
console.log(list1) // 出力結果: [1, 2, 3]
console.log(total) // 出力結果: 12
関数に切り出す
※ ↓は @noenture さんからのコメントを元に追記したテクニックですmm ありがとうございますmm
処理を即時関数に切り出すことで、可変変数のスコープを関数内に閉じ込めることができるため、後続の処理に影響を与えることがなくなるので良さそうです!
const list1 = [1, 2, 3]
const total = (total => {
for (let i = 0; i < list1.length; i++) {
total += list1[i] * 2
}
return total
})(0)
この場合再利用性があるものや単体テストを書きたいものについては名前付き関数できり出すのが良さそうですね
まずはできるところから
いきなり再代入禁止は難しいかと思いますので、ややこしくなりそうならば思い切って再代入してしまう、または処理後にconst化させるなど自分に合ったやりやすい形で実装してもらうのが良いかもしれません
またしてもScalaの話になってしまいますがScalaは現実主義であるため、valだけでなくちゃんとvarの選択肢を残したりしていたりと現実的な選択ができるための配慮がされているため、この辺りも見習っても良さそうだと個人的には思っています
なので冒頭にも述べたように**「できるところから再代入しないように実装していく」**というのが良いのかなと思っています
注意点としては、チーム開発の中で取り入れる場合、ふんわりした方針にしてしまうと人によって認識の齟齬が生じて実装が違ってきてしまうため、方針はちゃんと議論を重ねた上で具体例含めて明確にしておくのが良いかもしれません
一歩進んだ話(副作用について)
「immutable」について調べていると「副作用」と言う言葉も見かけるかもしれません
「immutable」と同様に「副作用」について考えてみると良いことがあるかもしれませんのでここでは頭出しの紹介だけしておきます
副作用とは、意図的出ない結果のことで、他の関数や機能に影響を与えてしまうことを「副作用がある」と言います
副作用を抑制するには以下を満たす必要があります
- 参照透過性(同じ条件を与えれば必ず同じ結果が得られる)
- 他のいかなる機能の結果にも影響を与えない(グローバル変数を使わない、メモリ操作をしないなど)
副作用があるものについて例
- グローバル変数がある
- 物理への読み書きがある
- メモリ
- 標準入出力
- ファイル入出力
- 他
とまあ説明はここぐらいまでにしておいて、副作用がないと当然意図しないことは起こりづらくなるため、結果バグの抑制に繋がります、また単体テストが書けると言うメリットもあります
「immutable」や「副作用」について追求して見ると奥が深いことに気がつくはずなのでどこまで取り入れるかは人次第かなと思っています
最後に
今回は「再代入しない」と言うことに割り切った話をしました
この記事を書いている中でリーダブルコードにも記載があることに気がついたので、改めてリーダブルコード凄えとなりつつ、昔読んでから月日が経ったため再度読み返して見るのはありかなと思っています
再代入しない良さについて自分の語彙力・表現力・理解力の限界があったため良さが伝わりづらかったかもしれませんが、コメントにて色々ご意見いただけると助かりますmm
また今回はJavaScriptでのサンプルとなっているため、若干JavaScriptに寄った話となっているかと思いますので他の言語でのテクニックなどももしあればコメントいただけたらと思いますmm