Mikatus Advent Calendar 2019 3日目の記事。
最初に
こんにちは。
普段はTypeScriptでVue.jsを書いているのですが、関数型言語を勉強しようと、最近社内ですごいHaskellたのしく学ぼう!
通称すごいH本
の勉強会を立ち上げました。
Haskellの特徴は色々あると思いますが、カリー化とか部分適用とか、JavaScriptに活かすのはなかなか難しそうだなと感じるものが多いので、わかりやすく効果がありそうな、値をイミュータブルに扱う方法
について紹介していきます。
私自身JavaScriptも入門レベルなので、紹介する内容は基礎的なものですが、同じように最近JavaScriptに入門した人がいたら参考にしてみてください。
では参ります。
イミュータブルのメリット
まずイミュータブルとは、Wikipediaさんによると、
イミュータブル (英: immutable) なオブジェクトとは、作成後にその状態を変えることのできないオブジェクトのことである。
イミュータブル - Wikipedia
らしいです。
プログラミング関係の言葉をWikipediaで調べると難解なことが多いのですが、これは比較的わかりやすいですね。
この定義ではわかりづらいという場合でも、後に出てくる実際のコードを見ればなんとなく理解できるのではないかと思います。
で、イミュータブルにプログラミングするメリットですが、
- 不測の値が混入するのを防ぐ
- 値が置き換えられてないかいちいち確認する手間や心理的負荷がなくなる
- プログラムの保守性が上がる
あたりが考えられ、特にチームで開発する上では大切なことだと感じています。
私自身独学期間を経て、今年エンジニアになったばかりなのですが、一人で開発する時には意識することもなかった、他のメンバー・将来のメンバーの負荷を減らすコードの必要性を実感しています。
というわけで、例外はあると思いますが、基本的に値はイミュータブルな方が良いのではないでしょうか。
では、これらのメリットを享受するため、JavaScriptでは実際どう書くのかを3つ見ていきましょう。
再代入しない
第一に、_一度変数を宣言したら、再代入しない_ということです。
JavaScriptでは変数を宣言する時に、var
、let
もしくはconst
が使えます。
var
はよっぽどの例外がない限り、使わないという方向で問題ないでしょう。
私にはよっぽどの例外が思いつきません。
残るは、let
とconst
ということになりますが、
- letは再代入可能
- constは再代入不可
になります。
実際のコードで見ていきましょう。
まずはlet
で変数を宣言した場合です。
let firstName = 'Ai'
let lastName = 'Katou'
firstName = 'Kai'
lastName = 'Atou'
let fullName = `${firstName} ${lastName}`
console.log(fullName)
// Kai Atou
// 書き換えられている!
加藤あいが阿藤快に書き換えられてしまっています。
次にconst
です。
const firstName = 'Lewis'
const lastName = 'Ann'
firstName = 'Rice'
// TypeError: Assignment to constant variable.
// 再代入しようとするとTypeErrorに
const fullName = `${firstName} ${lastName}`
console.log(fullName)
// Lewis Annが必ず表示される
const obj = { number: 11, firstName: 'Hinata', lastName: 'Kashiwagi' }
obj.number = 100
console.log(obj)
// {number: 100, firstName: 'Hinata', lastName: 'Kashiwagi'}
// あくまで再代入の禁止で、オブジェクトのプロパティ変更は可能なため注意
アン・ルイスを半ライスに書き換えようと思ったのですが、できませんでした。
というわけで変数の宣言にはconst
を使っていきましょう。
JavaScriptを本格的に書き始めて2ヶ月弱ですが、let
での宣言が必要になる機会はほぼないという印象です。
なお、const
でもあくまで再代入ができなくなるだけであって、オブジェクトのプロパティは変更できてしまうので注意しましょう。
次行きます。
配列をイミュータブルに扱う
今度は配列をイミュータブルに扱う方法です。
配列の操作には破壊的操作
と非破壊的操作
が存在します。
配列に対して破壊的操作
を行うと、操作を行なった配列が直接変更されてしまい、変更前の値を参照することができなくなります。
対して、非破壊的操作
は元の配列はそのままに新しい配列を返すので、変更前の値も参照することができます。
実際のコードで見ていきましょう。
const array = [3, 5, 6, 10, 11, 12]
const sortedArray = array.sort((a, b) => b - a)
console.log(array)
// [ 12, 11, 10, 6, 5, 3 ]
// 元の配列もソートされてしまっている!
sortメソッドは破壊的操作
なので元の配列もソートされてしまいます。
sortメソッドは便利なので、ぜひ使いたいところですが、このままではイミュータブルな状態を実現できません。
なので下記のように書いていきましょう。
const array = [3, 5, 6, 10, 11, 12]
const copiedArray = [...array]
// 元のarrayを変更しないようにスプレッド構文で一旦コピーする
const sortedArray = copiedArray.sort((a, b) => b - a)
console.log(array)
// [ 3, 5, 6, 10, 11, 12]
// ソート前の配列も呼び出せる
console.log(sortedArray)
// [ 12, 11, 10, 6, 5, 3 ]
sortメソッドを適用する前に配列をコピーしました。
一手間かかるだけのように思えますが、こうすることで上に挙げたメリットを享受できます。
const array = [3, 5, 6, 10, 11, 12]
array.push(13)
// pushも元の配列を変更する
const addedArray = [...array, 13]
// スプレッド構文を使って要素が追加された新しい配列を作る
ここではsort
とpush
メソッドを例に挙げていますが、破壊的操作は他にもあります。
配列にメソッドを適用しようと思ったら、それが破壊的
か非破壊的
かを確認してみましょう。
次行きます。
オブジェクトをイミュータブルに扱う
配列に続いて今度はオブジェクトです。
再代入しないの項目でも少し触れましたが、オブジェクトについてはconst
で宣言しようとプロパティの変更ができてしまいます。
ここもイミュータブルに扱いたいところです。
実際のコードで見ていきましょう。
const obj = { number: 11, firstName: 'Hinata', lastName: 'Kashiwagi' }
obj.color = 'orange'
// obj[color] = 'orange' も同様
console.log(obj)
// { number: 11, firstName: 'Hinata', lastName: 'Kashiwagi', color: 'orange' }
// 変更されてしまっていて、変更前の状態が参照できない
const addedObj = { ...obj, color: 'orange' }
// スプレッド構文を使って追加する
console.log(addedObj)
// { number: 11, firstName: 'Hinata', lastName: 'Kashiwagi', color: 'orange' }
console.log(obj)
// { number: 11, firstName: 'Hinata', lastName: 'Kashiwagi' }
// 元の状態も参照できる
元のオブジェクトのプロパティを直接変更するのではなく、スプレッド構文
を使って新しいオブジェクトを作っています。
スプレッド構文
便利ですね。
ただ、この例については、TypeScriptを導入しているなら、型をちゃんと定義することで、後からプロパティの追加とかができないようにする方がいいと思います。
#最後に
値をイミュータブルに扱う方法を3つ紹介しました。
意識したことがなかった人がいたら、ぜひ意識して今回紹介した方法などを試してみてください。
すごいH本勉強会では、まだモナド等には触れていないので、万が一モナドを理解できたらJavaScriptを書く際にも活かせないか模索したいと思います。
それでは!