現在、 React JS および React Native フレームワークに取り組んでいます。途中で、FacebookのFluxとReduxの実装について読んでいたときに、Immutabilityまたは Immutable-JS library に出会いました。
問題は、なぜ不変性がそれほど重要なのかということです。オブジェクトの変更の何が問題になっていますか?それは物事を単純にしませんか?
例を挙げて、簡単なNews readerアプリを考えてみましょう。オープニング画面はニュースの見出しのリストビューです。
たとえば、オブジェクトの配列と値を最初に設定した場合操作しないでください。それが、不変性の原則が言っていることですよね? (間違っている場合は修正してください。)しかし、更新する必要のある新しいNewsオブジェクトがある場合はどうなりますか?通常の場合、オブジェクトを配列に追加するだけでした。この場合、どうすれば達成できますか?ストアを削除して再作成しますか?配列にオブジェクトを追加することは、より安価な操作ではありませんか?
PS:この例が不変性を説明する正しい方法でない場合は、正しい実用的な例を教えてください。
私は最近、同じトピックを研究しています。私はあなたの質問に答え、これまでに学んだことを共有するように最善を尽くします。
問題は、なぜ不変性がそれほど重要なのかということです。オブジェクトの変更の何が問題になっていますか?それは物事を単純にしませんか?
基本的に、不変性は予測可能性とパフォーマンスを(間接的に)高め、変異の追跡を可能にするという事実に帰着します。
予測可能性
突然変異は変更を隠し、それにより(予期しない)副作用が発生し、厄介なバグを引き起こす可能性があります。不変性を強制すると、アプリケーションアーキテクチャとメンタルモデルをシンプルに保つことができ、アプリケーションについて推論しやすくなります。
パフォーマンス
不変オブジェクトに値を追加すると、既存の値をコピーする必要がある新しいインスタンスを作成し、メモリを消費する新しいオブジェクトに新しい値を追加する必要があることを意味しますが、不変オブジェクトは構造共有を利用してメモリを削減できますオーバーヘッド。
すべての更新は新しい値を返しますが、内部構造は共有され、メモリ使用量(およびGCスラッシング)を大幅に削減します。つまり、1000個の要素を持つベクトルに追加しても、実際には1001要素長の新しいベクトルは作成されません。ほとんどの場合、内部的には少数の小さなオブジェクトのみが割り当てられます。
詳細については、こちらをご覧ください こちら 。
突然変異追跡
メモリ使用量の削減に加えて、不変性により、参照と値の同等性を利用してアプリケーションを最適化できます。これにより、何か変更があったかどうかを簡単に確認できます。たとえば、反応コンポーネントの状態変化。 shouldComponentUpdate
を使用して、状態オブジェクトを比較することで状態が同一であるかどうかを確認し、不要なレンダリングを防ぐことができます。詳細については、こちらをご覧ください こちら 。
追加のリソース:
最初に値を持つオブジェクトの配列を設定するとします。操作できません。それが、不変性の原則が正しいと言っていることです(間違っている場合は修正してください)。しかし、更新する必要がある新しいNewsオブジェクトがある場合はどうなりますか?通常の場合、オブジェクトを配列に追加するだけでした。この場合、どうすれば達成できますか?ストアを削除して再作成しますか?配列にオブジェクトを追加することは、より安価な操作ではありませんか?
はい、これは正しいです。これをアプリケーションに実装する方法について混乱している場合は、 redux がどのようにこれを実行してコアの概念に慣れるかを確認することをお勧めします。
Reduxは不変性を含むため、例としてReduxを使用するのが好きです。単一の不変の状態ツリー(store
と呼ばれる)があり、すべての状態の変更は、アクションをディスパッチすることによって明示的に行われます。応用。あなたはそれの核心原理についてもっと読むことができます こちら 。
egghead.io には優れたreduxコースがあります。ここで Dan Abramov はreduxの作者であり、これらの原則を次のように説明しています(シナリオに合わせてコードを少し変更しました) ):
import React from 'react';
import ReactDOM from 'react-dom';
// Reducer.
const news = (state=[], action) => {
switch(action.type) {
case 'ADD_NEWS_ITEM': {
return [ ...state, action.newsItem ];
}
default: {
return state;
}
}
};
// Store.
const createStore = (reducer) => {
let state;
let listeners = [];
const subscribe = (listener) => {
listeners.Push(listener);
return () => {
listeners = listeners.filter(cb => cb !== listener);
};
};
const getState = () => state;
const dispatch = (action) => {
state = reducer(state, action);
listeners.forEach( cb => cb() );
};
dispatch({});
return { subscribe, getState, dispatch };
};
// Initialize store with reducer.
const store = createStore(news);
// Component.
const News = React.createClass({
onAddNewsItem() {
const { newsTitle } = this.refs;
store.dispatch({
type: 'ADD_NEWS_ITEM',
newsItem: { title: newsTitle.value }
});
},
render() {
const { news } = this.props;
return (
<div>
<input ref="newsTitle" />
<button onClick={ this.onAddNewsItem }>add</button>
<ul>
{ news.map( ({ title }) => <li>{ title }</li>) }
</ul>
</div>
);
}
});
// Handler that will execute when the store dispatches.
const render = () => {
ReactDOM.render(
<News news={ store.getState() } />,
document.getElementById('news')
);
};
// Entry point.
store.subscribe(render);
render();
また、これらのビデオは、以下の不変性を実現する方法をさらに詳しく示しています。
問題は、なぜ不変性がそれほど重要なのかということです。オブジェクトの変更の何が問題になっていますか?それは物事を単純にしませんか?
実際には、逆のことが言えます。少なくとも長期的には、可変性は事態をより複雑にします。はい、どこでも好きなように変更できるため、最初のコーディングが簡単になりますが、プログラムが大きくなると問題になります。値が変わったら、何が変わったのでしょうか。
すべてを不変にすると、データが突然変更されることはなくなります。関数に値を渡すと、その関数では値を変更できないことを確実に知っています。
簡単に言うと、不変の値を使用すると、コードについて推論するのが非常に簡単になります。誰もがデータの一意のコピーを取得するので、それで混乱したり、コードの他の部分を壊したりすることはできません。これにより、マルチスレッド環境での作業がどれほど簡単になるか想像してみてください!
注1:実行内容によっては、不変性に対するパフォーマンスコストが発生する可能性がありますが、Immutable.jsなどはできる限り最適化されます。
注2:万が一の場合、Immutable.jsとES6 const
の意味はまったく異なります。
通常の場合、オブジェクトを配列に追加するだけでした。この場合、どうすれば達成できますか?ストアを削除して再作成しますか?配列にオブジェクトを追加することは、より安価な操作ではありませんか? PS:この例が不変性を説明する正しい方法でない場合は、正しい実用的な例を教えてください。
はい、あなたのニュースの例は完璧によく、あなたの推論は正確です。既存のリストを修正することはできないので、新しいリストを作成する必要があります。
var originalItems = Immutable.List.of(1, 2, 3);
var newItems = originalItems.Push(4, 5, 6);
他の答えは問題ありませんが、実際のユースケースに関する質問(他の答えのコメントから)に取り組むには、実行中のコードの外側に1分間足を踏み入れ、鼻の真下のユビキタスな答えを見てみましょう:git。コミットをプッシュするたびに、リポジトリ内のデータを上書きするとどうなりますか?
今、私たちは不変コレクションが直面している問題の1つ、つまりメモリの膨張に直面しています。 Gitは、変更を加えるたびにファイルの新しいコピーを作成するだけではなく、単にdiffを追跡するだけのスマートです。
Gitの内部の仕組みについてはあまり知りませんが、参照しているライブラリの構造共有と同様の戦略を使用していると推測できます。内部では、ライブラリは tries または他のツリーを使用して、異なるノードのみを追跡します。
この戦略は、対数時間で動作する well-known tree-operationアルゴリズムがあるため、メモリ内データ構造に対しても合理的に機能します。
別のユースケース:Webアプリに[元に戻す]ボタンが必要だと言います。データの不変表現では、そのような実装は比較的簡単です。しかし、突然変異に依存している場合、世界の状態をキャッシュしてアトミックな更新を行うことを心配する必要があります。
要するに、実行時のパフォーマンスと学習曲線の不変性に対して支払うべき代償があります。しかし、経験豊富なプログラマなら誰でも、デバッグ時間はコード作成時間よりも桁違いに大きいと言うでしょう。また、実行時のパフォーマンスへのわずかな打撃は、ユーザーが耐える必要のない状態関連のバグを上回る可能性があります。
JavaScriptで不変性がそれほど重要(または必要)なのはなぜですか?
不変性はさまざまなコンテキストで追跡できますが、最も重要なことは、アプリケーションの状態およびアプリケーションUIに対して追跡することです。
JavaScript Reduxパターンは非常にトレンディでモダンなアプローチであり、あなたがそれについて言及したからです。
UIについては、予測可能にする必要があります。 UI = f(application state)
の場合は予測可能です。
アプリケーション(JavaScript)は、reducer関数を使用して実装されたアクションを介して状態を変更します。
レデューサー関数は単にアクションと古い状態を取り、新しい状態を返し、古い状態をそのまま保持します。
new state = r(current state, action)
利点は、すべての状態オブジェクトが保存されているため状態をタイムトラベルし、UI = f(state)
以降の任意の状態でアプリをレンダリングできることです。
これらのすべての状態を作成することは、メモリ効率がよくなる可能性があり、Gitとの類推は素晴らしいです。LinuxOSでも、シンボリックリンク(inodeに基づく)と同様の類推があります。
問題は、なぜ不変性がそれほど重要なのかということです。オブジェクトの変更の何が問題になっていますか?それは物事を単純にしませんか?
技術的な観点から見ると、可変性に問題はありません。高速で、メモリを再利用しています。開発者は最初から使用しています(覚えているとおり)。問題は、この使用がもたらす可能性のある可変性とトラブルの使用にあります。
オブジェクトが何かと共有されていない場合、たとえば、関数のスコープ内に存在し、外部に公開されていない場合、不変性の利点を確認することは困難です。本当にこの場合、不変であることは意味がありません。不変性の感覚は、何かが共有されたときに始まります。
変異性の頭痛
可変共有構造は、多くの落とし穴を簡単に作成できます。参照へのアクセス権を持つコードの任意の部分の変更は、この参照の可視性を持つ他の部分に影響を与えます。そのような影響は、異なるモジュールを認識してはならない場合でも、すべての部品を一緒に接続します。 1つの関数の突然変異は、アプリのまったく異なる部分をクラッシュさせる可能性があります。そのようなことは悪い副作用です。
突然変異に関する次の問題は、破損状態です。途中で突然変異手順が失敗し、一部のフィールドが修正され、一部のフィールドが修正されなかった場合、破損状態が発生する可能性があります。
さらに、突然変異では変化を追跡するのが困難です。単純な参照チェックでは、何が変更されたのかを知るために、違いは表示されません。また、変更を監視するには、いくつかの観察可能なパターンを導入する必要があります。
最後に、突然変異は信頼不足の理由です。構造を変更できる場合、その構造が価値を望んでいることを確認する方法。
const car = { brand: 'Ferrari' };
doSomething(car);
console.log(car); // { brand: 'Fiat' }
上記の例が示すように、可変構造を渡すことは、異なる構造を持つことで常に終了できます。関数doSomethingは、外部から与えられた属性を変更しています。コードに対する信頼はありません、あなたは本当にあなたが持っているものとあなたが持っているものを知りません。これらの問題はすべて、次の理由で発生します:可変構造がメモリへのポインタを表している。
不変性とは、同じオブジェクト、構造に対して変更が行われるのではなく、変更が新しいオブジェクト、構造で表されることを意味します。これは、参照がメモリポインタだけでなく値を表すためです。すべての変更は新しい値を作成し、古い値に影響を与えません。このような明確なルールは、信頼性とコードの予測可能性を返します。関数は、突然変異ではなく、独自の値を持つ独自のバージョンを扱うため、安全に使用できます。
メモリコンテナの代わりに値を使用すると、すべてのオブジェクトが特定の変更不可能な値を表し、それを使用しても安全であることが確実になります。
不変の構造は値を表しています。
私は中程度の記事で主題をさらに詳しく調べています- https://medium.com/@macsikora/the-state-of-immutability-169d2cd1131
Javascriptの不変性のもう1つの利点は、一時的なカップリングが減少することです。これは一般に設計に大きな利点があります。 2つのメソッドを持つオブジェクトのインターフェースを考えてみましょう。
class Foo {
baz() {
// ....
}
bar() {
// ....
}
}
const f = new Foo();
baz()
への呼び出しが正しく機能するためには、オブジェクトを有効な状態にするにはbar()
への呼び出しが必要な場合があります。しかし、これをどのように知っていますか?
f.baz();
f.bar(); // this is ok
f.bar();
f.baz(); // this blows up
それを理解するには、クラスの内部を精査する必要があります。これは、パブリックインターフェイスを調べてもすぐには明らかにならないからです。この問題は、多くの変更可能な状態とクラスを持つ大規模なコードベースで爆発する可能性があります。
Foo
が不変の場合、これはもはや問題ではありません。クラスの内部状態は変更できないため、baz
またはbar
を任意の順序で呼び出すことができると想定しても安全です。
私の他の答えは、非常に実用的な観点から質問に対処し、私はまだそれが好きです。私はこれをその答えに追加するのではなく、別の答えとして追加することにしました。それは退屈な哲学的暴言であり、質問にも答えることを望んでいますが、私の既存の答えには合わないからです。
小さなプロジェクトでも不変性は有用ですが、それが存在するのでそれがあなたのためのものであると仮定しないでください。
注:この回答の目的のために、私はいくつかの利益のために自己否定を意味するために単語「規律」を使用しています。
これは、「Typescriptを使用する必要がありますか?JavaScriptで型が非常に重要なのはなぜですか?」という別の質問に似ています。同様の答えもあります。次のシナリオを検討してください。
あなたは、約5000行のJavaScript/CSS/HTMLコードベースの唯一の著者であり、メンテナーです。あなたの準技術的な上司は、TypeScript-as-the-new-hotnessについて何かを読み、それに移行したいかもしれないと提案しますが、決定はあなたに任せます。だからあなたはそれについて読んだり、遊んだりする。
それで、あなたは作る選択をしました、あなたはTypescriptに移動しますか?
TypeScriptにはいくつかの魅力的な利点があります。インテリセンス、エラーを早期にキャッチし、APIを事前に指定し、リファクタリングが壊れたときに問題を修正しやすく、テストが少なくなります。 TypeScriptにはいくつかのコストもかかります。非常に自然で正しいJavaScriptイディオムは、特に強力ではない型システムでモデル化するのが難しい場合があります。アノテーションによってLoCが増大し、既存のコードベースを書き換える時間と労力、ビルドパイプラインの追加ステップなどがあります。より基本的には、コードがである可能性が高いという約束と引き換えに、可能性のある正しいJavaScriptプログラムのサブセットを作成します正しい。それはarbitrarily意的に制限されています。それが全体のポイントです:あなたは自分自身を制限する何らかの規律を課します(できれば足で自分自身を撃つことから)。
質問に戻り、上記のパラグラフの文脈で言い換えると:itworth it?
説明したシナリオでは、小規模から中規模のJSコードベースに精通している場合、TypeScriptを使用することは実用的というよりも審美的であると主張します。そして、それはfineであり、美学を備えたwrongはありません。必ずしも説得力がない。
シナリオB:
あなたは転職し、Foo Corpの基幹業務プログラマーになりました。10人のチームと90000 LoC(およびカウント)JavaScript/HTML/CSSコードベースで作業しており、babel、webpackを含むかなり複雑なビルドパイプラインを使用しています。 、ポリフィルのスイート、さまざまなプラグイン、状態管理システム、最大20のサードパーティライブラリ、最大10の内部ライブラリ、社内スタイルガイドのルールを備えたリンターのようなエディタープラグインなどと反応します。
あなたが5k LoCの男/女の子だったとき、それはそれほど重要ではありませんでした。ドキュメンテーションでさえそれほど大したことではなく、6ヶ月後にコードの特定の部分に戻っても十分簡単に理解できました。しかし今では、規律はニースだけでなく、必要なです。その規律にはTypeScriptが含まれない場合がありますが、willは何らかの形式の静的解析とその他の形式のコーディング規律(ドキュメント、スタイルガイド、ビルドスクリプト、回帰テスト、CI)。規律はluxuryではなく、necessityです。
これはすべて1978年のGOTO
に適用されました。CでのちょっとしたブラックジャックゲームはGOTO
sとスパゲッティロジックを使用できます。そしてもっと野心的で、まあ、undisciplinedGOTO
の使用は維持できませんでした。そして、これらすべては今日の不変性に当てはまります。
静的型と同様に、大規模なコードベースで保守/拡張を行うエンジニアのチームと作業していない場合、不変性を使用することは実用的というよりも審美的です:利点はまだありますが、まだコストを上回っていない可能性があります。
しかし、すべての有用な分野と同様に、それはもはやオプションではないという点があります。健康的な体重を維持したい場合、アイスクリームを含む規律はオプションかもしれません。しかし、競争力のあるアスリートになりたい場合、アイスクリームを食べるかどうかの選択は、目標の選択に含まれます。ソフトウェアで世界を変えたい場合、不変性は自重で崩壊しないようにするために必要なものの一部かもしれません。
昔々、スレッド間のデータ同期に問題がありました。この問題は大きな痛みであり、10以上の解決策がありました。一部の人々は根本的にそれを解決しようとしました。関数型プログラミングが生まれた場所でした。それはまるでマルクス主義のようなものです。ダンアブラモフがこのアイデアをどのようにJSに販売したかは、シングルスレッドであるため理解できませんでした。彼は天才です。
小さな例を挙げることができます。 gccには__attribute__((pure))
という属性があります。コンパイラーは、特に明確でない場合、関数が純粋であるかどうかを解決しようとします。状態が可変であっても、関数は純粋である場合があります。不変性は、関数が純粋であることを保証する100以上の方法の1つにすぎません。実際、関数の95%は純粋です。
実際に重大な理由がない場合は、制限(不変性など)を使用しないでください。何らかの状態を「元に戻す」場合は、トランザクションを作成できます。通信を簡素化する場合は、不変データを含むイベントを送信できます。それはあなた次第です。
私はポストマルクス主義共和国からこのメッセージを書いています。アイデアの急進化は間違った方法であると確信しています。