Clojure

値と変化: Clojureにおける同一性と状態へのアプローチ

多くの人は命令型言語からClojureに移行し、Clojureのやり方に直面すると戸惑う一方、より関数型のバックグラウンドを持つ人は、Clojureの関数型サブセットから離れると、Javaで見られるような状態に関する問題に直面すると考えています。 このエッセイは、命令型および関数型プログラムが世界をモデル化する際に直面する問題に対するClojureのアプローチを明らかにすることを目的としています。

命令型プログラミング

命令型プログラムは、その世界(例えばメモリ)を直接操作します。 それは、現在では持続不可能なシングルスレッドの前提、つまり、あなたがそれを見たり変更したりしている間、世界は停止しているという前提に基づいています。 あなたは「これをしなさい」と言えばそれが起こり、「それを変えなさい」と言えばそれが変わります。 命令型プログラミング言語は、これをしなさい/それをしなさいと言い、メモリ位置を変更することに重点を置いています。

これは、マルチスレッド以前でも、決して良い考えではありませんでした。 並行性を追加すると、実際の問題が発生します。なぜなら、「世界は停止している」という前提はもはや真実ではなく、その錯覚を回復することは非常に困難でエラーが発生しやすいためです。 全能であるかのように振る舞う複数の参加者は、どういうわけか、他の参加者の前提と影響を破壊することを避けなければなりません。 これには、ミューテックスとロックを使用して、各参加者が操作する領域を隔離し、共有メモリへの変更を伝播して他のコアから見えるようにするための多くのオーバーヘッドが必要です。 それはあまりうまく機能しません。

関数型プログラミング

関数型プログラミングは、世界をより数学的に捉え、プログラムを特定の値を受け取り、他の値を生成する関数と見なします。 関数型プログラムは、命令型プログラムの外部「効果」を避け、関数の活動は完全に局所的であるため、理解、推論、テストが容易になります。 プログラムの一部が純粋に機能する範囲では、調整する変更がないため、並行性は問題になりません。

動作モデルと同一性

コンパイラや定理証明器など、一部のプログラムは大規模な関数にすぎませんが、他の多くのプログラムはそうではありません。それらは動作モデルに似ており、そのため、この議論では**_同一性_**と呼ぶものをサポートする必要があります。 同一性とは、**_時間の経過とともに一連の異なる値に関連付けられた安定した論理エンティティ_**を意味します。 モデルは、人間がアイデンティティを必要とするのと同じ理由で、世界を表現するためにアイデンティティを必要とします。「今日」や「アメリカ」のようなアイデンティティが、常に単一の定数値を表す必要があるとしたら、どのように機能するでしょうか? アイデンティティとは名前を意味するものではありません(私は母をママと呼びますが、あなたはそうではないでしょう)。

したがって、この議論では、同一性は状態を持つエンティティであり、それはある時点でのその値です。 そして、**_値は変化しないもの_**です。42は変わりません。2008年6月29日は変わりません。 ポイントは移動せず、日付は変更されません。一部の悪いクラスライブラリがあなたに信じさせようとするものは関係ありません。 集約でさえ値です。 私の好きな食べ物のセットは変わりません。つまり、将来異なる食べ物を好む場合、それは異なるセットになります。

アイデンティティは、絶えず機能的にそれ自体の新しい値を作成している世界に連続性を重ね合わせるために使用する精神的なツールです。

オブジェクト指向プログラミング (OO)

OOは、とりわけ、プログラムでアイデンティティと状態をモデル化するためのツールを提供しようとする試みです(状態と動作の関連付け、およびここでは無視される階層分類も)。 OOは通常、アイデンティティと状態を統合します。つまり、オブジェクト(アイデンティティ)はその状態の値を含むメモリのポインタです。 アイデンティティとは独立して状態を取得する方法は、コピーする以外にありません。 他の人が変更するのをブロックせずに、安定した状態を観察する(コピーすることさえ)方法はありません。 アイデンティティの状態をインプレースメモリミューテーション以外に異なる値に関連付ける方法はありません。 言い換えれば、**_典型的なOOは命令型プログラミングを組み込んでいます!_** OOはこのようである必要はありませんが、通常はこのようになっています(Java / C ++ / Python / Rubyなど)。

OOに慣れている人は、プログラムがオブジェクトの値を変化させると考えています。 彼らは、42のような値の真の概念を、決して変わらないものとして理解していますが、通常、その値の概念をオブジェクトの状態に拡張しません。 それは彼らのプログラミング言語の失敗です。 これらの言語は、値のモデリングに、アイデンティティやオブジェクトと同じ構成体を使用し、デフォルトで可変性を使用するため、最も規律のあるプログラマー以外の人はすべて、必要以上のアイデンティティを作成し、値などにする必要があるものからアイデンティティを作成します。

Clojureプログラミング

別の方法があり、それはアイデンティティと状態を分離することです(ここでも、間接参照がプログラミングを救います)。状態の概念を「このメモリブロックの内容」から「このアイデンティティに現在関連付けられている**_値_**」に移行する必要があります。 したがって、アイデンティティは異なる時間に異なる状態になる可能性がありますが、_状態自体は変化しません_。 つまり、アイデンティティは状態ではなく、アイデンティティは状態を**_持つ_**ものです。 いつでも正確に1つの状態。 そして、その状態は真の値です。つまり、決して変わりません。 アイデンティティが変化するように見える場合は、時間の経過とともに異なる状態値に関連付けられるようになるためです。 これがClojureモデルです。

Clojureのモデルでは、値の計算は純粋に機能的です。 値は決して変わりません。 新しい値は、突然変異ではなく、古い値の関数です。 しかし、論理的なアイデンティティは、値へのアトミック参照(Refs and Agents)によって適切にサポートされています。 参照への変更は、システムによって制御/調整されます。つまり、協力はオプションではなく、手動でもありません。 世界は参加者とプログラミング言語/システムの協力的な努力によって前進し、Clojureは世界の一貫性管理を担当しています。 参照の値(アイデンティティの状態)は、調整なしで常に観察可能であり、スレッド間で自由に共有できます。

参加者(スレッド)が1人だけの場合でも、このようにプログラムを構築する価値があります。 関数値の計算がアイデンティティ/値の関連付けから独立している場合、プログラムは理解/テストが容易になります。 そして、他の参加者が必要になった場合(必然的に)、簡単に追加できます。

並行性

並行性を扱うということは、全能の錯覚をあきらめるということです。 プログラムは、他の参加者がいることを認識し、世界は変化し続けることを認識する必要があります。 したがって、プログラムは、いくつかのアイデンティティの状態の値を観察する場合、その後で新しい状態を取得できるため、スナップショットが得られることを理解する必要があります。 しかし、多くの場合、それは意思決定やレポートの目的に十分です。 私たちは人間は、感覚システムによって提供されるスナップショットで非常にうまくやっています。 良いことは、そのような状態値は不変であるため、処理中に手元で変化しないことです。

一方、状態を新しい値に変更するには、「現在」の値とアイデンティティにアクセスする必要があります。 ClojureのRefsとAgentsはこれを自動的に処理します。 Refsの場合、行うすべてのインタラクションはトランザクション内で発生する必要があり(そうでない場合、Clojureは例外をスローします)、そのようなすべてのインタラクションは、ある時点の世界の整合性のあるビューを確認し、変更される状態が変更されていない限り、変更は行われません。その間に他の参加者によって。 トランザクションは、複数のRefsへの同期変更をサポートします。 一方、Agentsは、単一の参照への非同期変更を提供します。 関数と値を渡すと、将来のある時点で、その関数はAgentの現在の状態を渡され、関数の戻り値がAgentの新しい状態になります。

すべての場合において、プログラムは世界の値の安定したビューを確認します。これらの値は変更できず、コア間で共有しても問題ないためです。 問題は、「値は決して変わらない」ということは、古い値から新しい値を作成することが効率的でなければならないということであり、Clojureでは永続的なデータ構造のために効率的です。 これらにより、不変性を優先するという、しばしば提供されるアドバイスに従うことができます。 したがって、現在の値を読み取り、その値に対して純粋関数を呼び出して新しい値を作成し、その値を新しい状態として設定することにより、アイデンティティの状態を新しい状態に設定します。 これらの複合操作は、altercommute、およびsend関数によって簡単かつアトミックになります。

メッセージパッシングとアクター

アイデンティティと状態をモデル化する方法は他にもあり、その中でも特に人気のあるものがメッセージパッシングのアクターモデルです。アクターモデルでは、状態はアクター(アイデンティティ)にカプセル化され、メッセージ(値)の受け渡しによってのみ影響を受けたり、見たりすることができます。非同期システムでは、アクターの状態のある側面を読み取るには、リクエストメッセージを送信し、レスポンスを待ち、アクターがレスポンスを送信する必要があります。アクターモデルは、分散プログラムの問題に対処するために設計されたことを理解することが重要です。そして、分散プログラムの問題ははるかに困難です。複数の世界(アドレス空間)が存在し、直接的な観察は不可能であり、相互作用は信頼性の低いチャネルを介して行われる可能性があるなどです。アクターモデルは透過的な分散をサポートします。すべてのコードをこの方法で記述すれば、他のアクターの実際の場所に縛られることはなく、コードを変更することなくシステムを複数のプロセス/マシンに分散させることができます。

Clojureで同一プロセス内の状態管理にアクターモデルを使用しないことを選択した理由はいくつかあります。

  • これははるかに複雑なプログラミングモデルであり、最も単純なデータ読み取りにも2メッセージの会話が必要であり、ブロッキングメッセージの受信を強制するため、デッドロックの可能性が生じます。分散の障害モードに対応したプログラミングは、タイムアウトなどを利用することを意味します。プログラムプロトコルの分岐を引き起こし、一部は関数で表され、その他はメッセージの値で表されます。

  • 同一プロセス内にあることの効率性を最大限に活用できません。大きな不変データ構造をスレッド間で効率的に直接共有することは可能ですが、アクターモデルは介在する会話と、場合によってはコピーを強制します。読み取りと書き込みはシリアル化され、互いにブロックします。

  • モデリングの柔軟性が低下します。これは、誰もが窓のない部屋に座って、郵便でのみコミュニケーションをとるような世界です。プログラムは、ブロッキングswitch文の積み重ねとして分解されます。受信を予期していたメッセージしか処理できません。複数のアクターが関与するアクティビティの調整は非常に困難です。協力/調整なしに何も観察できないため、アドホックなレポートや分析は不可能になり、代わりにすべてのアクターが各プロトコルに参加することを強制されます。

  • ローカルでうまく機能するものを透過的に分散させても、うまくいかないことがよくあります。会話の粒度が細かすぎたり、メッセージのペイロードが大きすぎたり、障害モードによって最適な作業分割が変更されたりするため、つまり、透過的な分散は透過的ではなく、コードをとにかく変更する必要があります。

Clojureは最終的には分散プログラミングのためにアクターモデルをサポートするかもしれませんが、分散が必要な場合にのみ代償を払うことになります。しかし、同一プロセスプログラミングには非常に煩わしいと思います。もちろん、YMMV(あなたの走行距離は異なる場合があります)。

まとめ

Clojureは、プログラムをモデルとして明示的にサポートする関数型言語であり、並行性に対応した単一プロセス内でアイデンティティと状態を管理するための堅牢で使いやすい機能を提供します。

オブジェクト指向言語からClojureに移行する場合、オブジェクトの代わりに、永続的なコレクション(例:マップ)のいずれかを使用できます。可能な限り値を使用してください。そして、オブジェクトが真にアイデンティティをモデル化している場合(このように考え始めると、実際にはあなたが思っているよりもはるかに少ないケースです)、状態が変化するアイデンティティをモデル化するために、状態としてマップなどを備えたRefまたはAgentを使用できます。値の詳細をカプセル化または抽象化したい場合は、それが重要でない場合は、それらを表示および操作するための一連の関数を記述することをお勧めします。ポリモーフィズムが必要な場合は、Clojureのマルチメソッドを使用してください。

ローカルの場合、Clojureには可変なローカル変数がないため、値を変化させるループで値を構築する代わりに、recurまたはreduceを使用して関数的に実行できます。