LoginSignup
327
244

More than 3 years have passed since last update.

なぜ昨今のJavaScriptではイミュータブルであるべきと言えるのか歴史的背景を踏まえて言語化する

Last updated at Posted at 2019-09-17

先日JavaScriptに慣れていない人のコードをレビューする機会があり、constで宣言されたオブジェクト内部に副作用を与えている記述がありました。

その時に「今の動作に問題ないけど、今風のJSならイミュータブルの方が良いかも」と指摘したものの、JSに疎い人からすれば背景が分からないはずで、理由を自分なりに説明したものの案外言語化が難しかったことがありました。

難しい理由として、イミュータブルであることは実利面と同時に、Facebook発祥のトレンドという側面も多分に含んでおり、JavaScript自体の潮流も踏まえておく必要があるからです。

今回は実利面に加えてトレンド面も交えて、なぜイミュータブル性がJavaScriptで重宝されるのかを見ていきましょう。

フロントエンドの世界では状態を持ち、時間やインタラクションと共に変化するから

サーバーサイドの世界から見た場合、HTTPはステートレスで状態を持ちません。Cookieによってステートフルにすることはできるものの、スケーラビリティの観点からAPI設計はステートレスであることが望まれます。

一方で動的なWebサイトやネイティブアプリでは状態を持ち、時間経過やユーザーのインタラクションによって状態を変化させることで、シームレスに描画を変化する必要があります。

描画内容は、クラス変数にせよ専用のstateにせよ何処かの場所に保持されている値を元に生成されています。そして画面が動的に変化するということは、値が何らかの形式で変化したことを意味します。

つまりフロントエンドではバックエンドと異なり、永続的な状態がどこかに存在し、非同期に発生する一定の条件によって状態が変化することになります。

この状態と描画の整合性を図ることがフロントエンド特有の難しさであり、jQueryに代表される命令的な実装から、Reactに代表される宣言的な実装へとシフトしている要因となっています。

jQueryからReactによってなぜイミュータブル性が伴うのかと思うかもしれません。これは一般論としてのイミュータブル以上に、Reactやそのエコシステムがイミューブルであることを要求していることにも端を発しています。

まずはフレームワークの話題に触れる前に、"状態を持つ"フロントエンドの世界におけるイミュータブル性についてもう少し深く考えてみましょう。

JavaScriptのオブジェクトは破壊的な変更による影響が予測しづらくなる

以下の記事に詳しいですが、JavaScriptではNumberStringのようなプリミティブ値を変更しても代入元への影響はありませんが、配列やクラスインスタンスのようなオブジェクトの場合、値を書き換えた時に代入元にも影響が及んでしまいます。

実際にコードを見てみましょう。確かにプリミティブ値を再代入しても、代入元であるnumberAの値は変わりません。

const numberA = 10;
let numberB = numberA;
numberB = 20;
console.log(numberA); // -> 10のまま

しかしオブジェクトの値を変更すると、代入元に対しても影響が及びます。これは関数の引数として渡した時にも同じです。

const objA = { hoge: 10 };
const objB = objA;
objB.hoge = 20;
console.log(objA); // -> { hoge: 20 }

もしも同一のオブジェクト(への参照)に対して、複数の変数が参照している場合、片一方の破壊的な変更が他にも及んでしまうため、予期せぬ描画が発生したりといった不具合につながります。

そして先ほど述べたようにフロントエンドはその性質上、動的に画面を描画するには状態を変更する必要があるため、こうした副作用の影響が及びやすいのです。

これを解決するための方法となるのが、値をイミュータブルにすることです。一度代入した値を変更したい場合は、値が置き換わった新しいオブジェクトを生成します。

const objA = { hoge: 10 };
let objB = objA;
objB = { ...objA, hoge: 20 };
console.log(objA); // -> { hoge: 10 }
console.log(objB); // -> { hoge: 20 }

スプレッド構文を使って新たなオブジェクトを生成することで、同一の参照を保持しなくなり、結果的にobjAの値も書き換わることがなくなりました。

これによって値の変化に伴う描画の変化がobjBに由来するものだけとなり、同じ参照を複数保持している時と比べて予測可能性が高まります。

今回はシンプルな例でしたが、アプリケーションの機能要件が増すほど複雑性も増すので、変数をイミュータブルにすることは挙動の予測性を高め、予期せぬ不具合が減ることが期待されます。

余談: マルチスレッド下のスレッドセーフはフロントエンドでは関係がない

一般論としてイミュータブルのメリットとして、オブジェクトが不変であるためマルチスレッド時のスレッドセーフが実現できることが挙げられます。

しかしJavaScriptではマルチスレッドをサポートするWebWorkerではDOMやLocalStorageの操作を行うことができず、元来の性質からスレッドセーフになっています。

ですからマルチスレッド対応としてのメリットはJavaScriptには関係がなく、後述するトレンドと伴って、JSのイミュータブル性は他言語とは異なる文脈があります。

Reactによるパラダイムシフトとイミュータブルの要求

改めて言及するまでもないかもしれませんが、JavaScriptではjQueryに代表される命令的な記述からReactに代表される宣言的な記述に、一定の機能要件を持つアプリケーションでは確実に潮流がシフトしています。

理由としてはある要素をクリックしたら、ある要素の外観を変更するといった命令を各所に記述する方式では、アプリケーションの肥大化と共に人間が仕様を把握することが困難となり、修正コストが線形に上がるほか、仕様の把握漏れによる不具合が発生する確率も上がるでしょう。

そこでVirtual DOMによるレンダリングの最適化を背景に、Reactが状態をViewへ(素直に)反映させる宣言的UIを実現し、従来の辛い開発体験も相まってパラダイムシフトが起きました。またFacebookはFluxアーキテクチャを提唱し、Reactのエコシステムもそれに追随していきました。

React開発チームの指向からか関数型の思想が随所に散りばめられていますし、そもそもReactでは状態に副作用を起こして再描画することができません。(今回はHooksをあえて使わず、従来的なクラスコンポーネントで説明します)

例えばある要素がクリックされたら、開閉式のとある要素を開くといったことを実現するために、状態を意味するstateを変更します。

class Component extends React.Component<Props, State> {
  constructor(props) {
    super(props);
    this.state = { 
      message: null,
      open: false 
    };
  }

  onClick() {
    this.setState({ ...this.state, open: true  });
  }
}

Reactでは状態であるstateに副作用を与えて再描画することは許容されず、常にイミュータブルにしておく必要があります。そしてイミュータブルであることで、プログラマは状態がどのように変化したかを検知することができ、結果として不要な再描画を防ぐといったことも実現できます。

shouldComponentUpdate(nextProps, nextState) {
  return nextProps.id !== this.props.id; // -> 返り値がfalseなら描画がスキップされる
}

こうしたイミュータブル性はReact本体に留まらず、エコシステムにまで波及しており、例えばReduxにおいても副作用による再レンダリングはできず、常にイミュータブルでなければなりません。

const articles = (state=[], action) => {
  switch(action.type) {
    case 'ADD_ARTICLES': {
      return [ ...state, ...action.articles ];
    }
    default: {
        return state;
    }
  }
};

またstateがイミュータブルであるため、undo/redoを実現できるので、少なくともSPA開発においては開発体験を確実に高めてくれます。

ではこうしたイミュータブル性は宣言的な記述に絶対求められるかというとそうではなく、むしろReact開発チームの志向によるものが大きいでしょう。

ともかく従来の命令的記述からパラダイムシフトを起こし、それがフロントエンド開発者に受け入れられていることは「JavaScriptの今風な書き方」というものが仮にあるのなら、切り離せない出来事と言えます。

(※今回はReactにフォーカスを当ててパラダイムの変化を捉えており、AngularやVue.jsの動向に言及していないことに歯がゆさを感じますが、イミュータブルというトピックゆえご容赦ください)

「単なるブーム」と捉える人もいる

こちらも余談になりますが、「イミュータブルは必要性よりもブームによるもの」と捉えている人もいます。

代表的なものとしてこちらのStackOverflowの回答がありますが、抜粋して内容を紹介します。

Short answer: Immutability is more a fashion trend than a necessity in JavaScript. There are a few narrow cases when it becomes useful (React/Redux mostly), though usually for the wrong reasons.

訳: 端的に言えば、イミュータビリティはJavaScriptにおいては必要性というよりも流行です。通常は間違った理由からですが、それが有用であるケースもわずかに存在します。(大抵はReact/Reduxです)

The timing was perfect. The novelty of Angular was fading, and JavaScript world was ready to fixate on the latest thing that had the right degree of cool, and this library was not only innovative but slotted in perfectly with React which was being peddled by another Silicon Valley powerhouse.

訳: (Immutabilityを背景としたReduxが登場した)タイミングが完璧でした。Angularの目新しさがなくなり、JSの世界ではクールで最新のものに固執していました。このライブラリは単に革新的だっただけでなく、FacebookによるReactに完璧に取り込まれていました。

In fact programmers have been mutating objects for er... as long as there has been objects to mutate. 50+ years of application development in other words.

And why complicate things? When you have object cat and it dies, do you really need a second cat to track the change? ...

訳: プログラマは50年以上の開発に渡って、変化させるオブジェクトがあればそれを変化させてきました。なぜ物事を複雑にする必要があるのでしょうか? 猫のオブジェクトがあり、それが死んだ時、変化を捉えるために二匹目の猫が本当に必要ですか?


こちらの回答は全体的にかなり強めの主張ですが、イミュータブルへの傾倒に対する疑問として一理あります。

元を辿ればReactとエコシステムがもたらしたイミュータブルへの誘いですが、現在ではフロントエンドだけでなく、(バックエンドを含めた)JavaScript全体に波及しており、それが果たして必要性のみに由来しているかといえばそうではないかもしれません。

スプレッド構文との相性の良さ

上記の疑問に対する一つの反論材料として、ES2015から登場したスプレッド構文によってオブジェクトをイミュータブルに扱うことが容易になり、プログラマの負担は(複雑なオブジェクトでない限り)小さなものになっています。

先ほどの例にも出していますが、オブジェクトの一部を書き換えたい場合、次のように記述します。

const objA = { key1: 1, key2: 2, key3: 3 };
const objB = { ...objA, key1: 10 }; // -> { key1: 10, key2: 2, key3: 3 }

これは配列にも適用可能です。

const arrA = [1, 2, 3, 4];
const arrB = [...arrA, 5]; // -> [1, 2, 3, 4, 5]

ドット3つである...を使うことでオブジェクトを展開することができ、特定のキーを上書きしたり、配列に要素を追加した新たなオブジェクトを生成できます。

このスプレッド構文がイミュータブルに変数を扱いやすくしているため、ミュータブルと比較してプログラマの負担が必ずしも大きいわけではありません。

それと比較して、イミュータブルによって値が予測可能となるメリットが上回っていると判断され、フロントエンド以外でもイミュータブル性を担保するケースが増えているのではないでしょうか。

パフォーマンスに対する懸念と実情

イミュータブルに対する批判としてパフォーマンス低下への懸念があります。確かにオブジェクトが肥大するほどパフォーマンスは劣化しますが、それは恐らく多くの人が考えているよりも小さいはずです。

オブジェクトの場合、変数が直接値を保持するのではなく、値への参照を保持します。ですから次のようなケースでもパフォーマンスが有意に変わるわけではありません。

const shortText = "1";
const longText = Array.from({length: 10000})
                 .map(() => '1').join(""); // -> "1"が1万個連なったもの

// それぞれの文字を1000個保持する配列を定義
const shortTexts = Array.from({length: 1000}).map(() => shortText);
const longTexts = Array.from({length: 1000}).map(() => longText);

// それぞれの配列をもとに、イミュータブルな配列を生成するコストは変わらない
const newShortTexts = [...shortTexts, "1"];
const newLongTexts = [...longTexts, "1"];

値への参照を保持すると考えていただければ、イミュータブルであるコストがさほど大きいわけではないと理解して頂けるのではないでしょうか。

実際にReduxを用いてSPAを開発していて、イミュータブルであること自体がパフォーマンス上のネックになったケースに遭遇したことはありません。

もちろんオブジェクトが肥大すればその分だけパフォーマンスは劣化しますが、それは程度問題と言えるでしょう。

immutable.jsによる解決

仮にパフォーマンスが問題になるケースがあるのならば、immutable.jsを利用すれば、パフォーマンスが格段に改善することが期待されます。イミュータブルに値を変更する処理が内部で効率化されているためです。

ただしimmutable.jsが扱うオブジェクトはpureJSではなく、Pureなオブジェクトに変換するコストがかかることや、特定ライブラリにロックインするリスクが存在します。

個人的にはパフォーマンス制約や、あまりに複雑なオブジェクトをイミュータブルに扱う面倒さが顕在化しない限りはスプレッド構文で十分だと考えていますが、パフォーマンスに懸念があっても、ライブラリによってそれを解決することができます。

最後に

JavaScriptにおけるイミュータブルを説明するには回りくどくなってしまいましたが、「なぜイミュータブルなのか」を言語化するに辺り、これらの背景を踏まえておく必要があるかと思い書かせて頂きました。

最後に個人的な考えを述べさせてもらうと、イミュータブルは確かに流行の側面があるものの、それを実現するコストが低く抑えられることに対して、もたらされるメリットはそれを上回るため、Reactあるいはフロントエンドの世界から離れても積極的に活用しています。

これらの説明が皆様にとって納得感のあるもので、新たな気付きになれば幸いです。

327
244
5

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
327
244