リアクティブ プログラミングに関するウィキペディアの記事を読みました。関数型リアクティブ プログラミングに関する小さな記事も読みました。説明はかなり抽象的です。
- 関数型リアクティブ プログラミング (FRP) とは実際には何を意味するのでしょうか?
- リアクティブ プログラミング (非リアクティブ プログラミングとは対照的に?) は何で構成されていますか?
私のバックグラウンドは命令型/オブジェクト指向言語であるため、このパラダイムに関連する説明をいただければ幸いです。
リアクティブ プログラミングに関するウィキペディアの記事を読みました。関数型リアクティブ プログラミングに関する小さな記事も読みました。説明はかなり抽象的です。
私のバックグラウンドは命令型/オブジェクト指向言語であるため、このパラダイムに関連する説明をいただければ幸いです。
純粋な関数型プログラミングでは、副作用はありません。多くの種類のソフトウェア (たとえば、ユーザーの操作を伴うもの) では、ある程度の副作用が必要です。
関数型スタイルを維持しながら副作用のような動作を得る 1 つの方法は、関数型リアクティブ プログラミングを使用することです。これは、関数型プログラミングとリアクティブ プログラミングの組み合わせです。(あなたがリンクしたウィキペディアの記事は後者に関するものです。)
リアクティブ プログラミングの背後にある基本的な考え方は、「時間の経過に伴う」値を表す特定のデータ型があるということです。これらの時間とともに変化する値を含む計算は、時間とともに変化する値を持ちます。
たとえば、マウスの座標を経時整数値のペアとして表すことができます。次のようなものがあるとしましょう (これは疑似コードです):
x = <mouse-x>;
y = <mouse-y>;
任意の時点で、x と y はマウスの座標になります。非反応的なプログラミングとは異なり、この割り当てを一度行うだけで、x 変数と y 変数は自動的に「最新」のままになります。これが、リアクティブ プログラミングと関数型プログラミングが非常にうまく連携する理由です。リアクティブ プログラミングを使用すると、変数を変更する必要がなくなりますが、変数の変更で実現できる多くのことを実行できます。
これに基づいていくつかの計算を行うと、結果の値も時間とともに変化する値になります。例えば:
minX = x - 16;
minY = y - 16;
maxX = x + 16;
maxY = y + 16;
この例でminX
は、常にマウス ポインターの x 座標よりも 16 少なくなります。リアクティブ対応ライブラリを使用すると、次のように言えます。
rectangle(minX, minY, maxX, maxY)
また、マウス ポインターの周りに 32x32 のボックスが描画され、マウス ポインターがどこに移動しても追跡されます。
これは、関数型リアクティブ プログラミングに関する非常に優れた論文です。
それがどのようなものかについて最初の直感に到達する簡単な方法は、プログラムがスプレッドシートであり、すべての変数がセルであると想像することです。スプレッドシート内のいずれかのセルが変更されると、そのセルを参照するセルも変更されます。FRPも同じです。ここで、一部のセルが独自に変化する (または、外部から取得される) と想像してください。GUI の状況では、マウスの位置が良い例です。
それは必然的にかなり多くを逃します。実際に FRP システムを使用すると、比喩はすぐに崩壊します。1 つには、通常、個別のイベント (マウスのクリックなど) もモデル化する試みがあります。ここに載せるのは、それがどのようなものかを理解してもらうためだけです。
私にとって、それはシンボルの約2つの異なる意味です=
:
x = sin(t)
的な意味では、それx
はの別の名前ですsin(t)
。だから書くことx + y
は と同じことですsin(t) + y
。関数型リアクティブ プログラミングは、この点で数学に似てx + y
いますt
。x = sin(t)
、代入: 代入時に取得した値x
を格納することを意味します。 sin(t)
OK、背景知識とあなたが指摘したウィキペディアのページを読むと、リアクティブプログラミングはデータフローコンピューティングのようなものですが、特定の外部「刺激」が一連のノードを起動して計算を実行するように見えます。
これは、UI 設計に非常に適しています。たとえば、ユーザー インターフェイス コントロール (音楽再生アプリケーションのボリューム コントロールなど) に触れると、さまざまな表示項目とオーディオ出力の実際のボリュームを更新する必要がある場合があります。有向グラフのノードに関連付けられた値の変更に対応するボリューム (スライダーなど) を変更する場合。
その「ボリューム値」ノードからのエッジを持つさまざまなノードが自動的にトリガーされ、必要な計算と更新がアプリケーション全体に自然に波及します。アプリケーションは、ユーザーの刺激に「反応」します。関数型リアクティブ プログラミングは、このアイデアを関数型言語で、または一般的には関数型プログラミング パラダイム内で実装したものにすぎません。
「データフロー コンピューティング」の詳細については、ウィキペディアでこの 2 つの単語を検索するか、お気に入りの検索エンジンを使用してください。一般的な考え方は次のとおりです。プログラムはノードの有向グラフであり、それぞれが簡単な計算を実行します。これらのノードは、一部のノードの出力を他のノードの入力に提供するグラフ リンクによって相互に接続されます。
ノードが起動またはその計算を実行すると、その出力に接続されたノードには、対応する入力が「トリガー」または「マーク」されます。すべての入力がトリガー/マーク/使用可能になっているノードは、自動的に起動します。グラフは、リアクティブ プログラミングがどのように実装されているかに応じて、暗黙的または明示的になる場合があります。
ノードは並列に起動していると見なすことができますが、多くの場合、連続して実行されるか、並列処理が制限されます (たとえば、それらを実行するスレッドがいくつかある場合があります)。有名な例は、Manchester Dataflow Machineで、(IIRC) はタグ付きデータ アーキテクチャを使用して、1 つ以上の実行ユニットを介してグラフ内のノードの実行をスケジュールしました。データフロー コンピューティングは、実行をクロック (または複数のクロック) で管理しようとするよりも、計算を非同期にトリガーして計算のカスケードを発生させる方が適切に機能する状況に非常に適しています。
リアクティブ プログラミングは、この「実行のカスケード」のアイデアをインポートし、データフローのような方法でプログラムを考えているように見えますが、一部のノードが「外の世界」にフックされ、実行のカスケードがこれらの感覚が発生したときにトリガーされるという条件付きです。のようなノードが変化します。プログラムの実行は、複雑な反射弧に似たものに見えます。プログラムは、刺激間で基本的に固着している場合もそうでない場合もあり、刺激間で基本的に固着している状態に落ち着く場合もあります。
「非反応的」プログラミングとは、実行の流れと外部入力との関係について非常に異なる見方でプログラミングすることです。人は、外部入力に反応するものは何でも「反応する」と言いたくなる傾向があるため、やや主観的なものになる可能性があります。しかし、物事の精神を見ると、一定の間隔でイベント キューをポーリングし、見つかったイベントを関数 (またはスレッド) にディスパッチするプログラムは、反応性が低くなります (一定の間隔でのユーザー入力のみに対応するため)。繰り返しになりますが、これがここでの精神です。非常に低いレベルでシステムに短いポーリング間隔でポーリングの実装を配置し、その上でリアクティブな方法でプログラムすることを想像できます。
FRP に関する多くのページを読んだ後、私は最終的に FRP についてのこの啓発的な記述に出くわし、最終的に FRP が実際に何であるかを理解するようになりました.
以下、Heinrich Apfelmus (Reactive Banana の著者) の言葉を引用します。
関数型リアクティブプログラミングの本質とは?
一般的な答えは、「FRP はシステムを変更可能な状態ではなく、時変する機能の観点から記述することです」というものであり、それは間違いではありません。これがセマンティックの観点です。しかし、私の意見では、次の純粋な構文基準によって、より深く、より満足のいく答えが得られます。
関数型リアクティブ プログラミングの本質は、宣言時に値の動的な動作を完全に指定することです。
たとえば、カウンターの例を見てみましょう。「上へ」と「下へ」というラベルの付いた 2 つのボタンがあり、カウンターを増減するために使用できます。命令的には、最初に初期値を指定してから、ボタンが押されるたびにそれを変更します。このようなもの:
counter := 0 -- initial value on buttonUp = (counter := counter + 1) -- change it later on buttonDown = (counter := counter - 1)
ポイントは、宣言時にカウンターの初期値のみを指定することです。counter の動的動作は、プログラム テキストの残りの部分で暗示されます。対照的に、関数型リアクティブ プログラミングでは、次のように、宣言時に動的な動作全体を指定します。
counter :: Behavior Int counter = accumulate ($) 0 (fmap (+1) eventUp `union` fmap (subtract 1) eventDown)
カウンターのダイナミクスを理解したいときはいつでも、その定義を見るだけで済みます。それに起こりうることはすべて右側に現れます。これは、後続の宣言が以前に宣言された値の動的な動作を変更できる命令型アプローチとは非常に対照的です。
したがって、私の理解では、FRP プログラムは一連の方程式です。
j
離散的です: 1,2,3,4...
f
依存するt
ため、これには外部刺激をモデル化する可能性が組み込まれています
プログラムのすべての状態は変数にカプセル化されますx_i
FRP ライブラリは進行時間、つまり を処理j
しj+1
ます。
これらの方程式については、このビデオで詳しく説明しています。
編集:
最初の回答から約 2 年後、最近、FRP の実装には別の重要な側面があるという結論に達しました。キャッシュの無効化という重要な実際の問題を解決する必要があります (通常は解決します) 。
x_i
-sの式は、依存関係グラフを表します。ある時点でいくつかのx_i
変更があった場合j
、他のすべてのx_i'
値をj+1
更新する必要はありません。そのため、すべての依存関係を再計算する必要はありx_i'
ませんx_i
。
さらに、x_i
変更を行う -s は増分的に更新できます。たとえばf=g.map(_+1)
、Scala で map 操作を考えてみましょう。 wheref
とg
are List
ofInts
です。ここではと が対応していf
ます。に要素を追加すると、 のすべての要素に対して操作を実行するのは無駄になります。一部の FRP 実装 ( reflex-frpなど) は、この問題を解決することを目的としています。この問題は、インクリメンタル コンピューティングとも呼ばれます。x_i(t_j)
g
x_j(t_j)
g
map
g
つまり、x_i
FRP の動作 ( -s ) は、キャッシュされた計算と考えることができます。x_i
一部のキャッシュが変更された場合に、これらのキャッシュ (キャッシュ) を効率的に無効にして再計算するのは、FRP エンジンのタスクですf_i
。
免責事項:私の答えはrx.jsのコンテキストにあります-Javascript用の「リアクティブプログラミング」ライブラリです。
関数型プログラミングでは、コレクションの各項目を反復処理する代わりに、コレクション自体に高階関数 (HoF) を適用します。したがって、FRP の背後にある考え方は、個々のイベントを処理する代わりに、イベントのストリームを作成し (observable* で実装)、代わりにそれに HoF を適用するというものです。このようにして、パブリッシャーをサブスクライバーに接続するデータ パイプラインとしてシステムを視覚化できます。
オブザーバブルを使用する主な利点は次のとおりです
。i) コードから状態を抽象化します。たとえば、イベント ハンドラを「n」番目のイベントごとにのみ発生させたい場合、または最初の「n」イベント後に発生を停止したい場合、または、最初の「n」個のイベントの後にのみ発火を開始する場合は、カウンターの設定、更新、およびチェックの代わりに、HoF (それぞれフィルター、takeUntil、スキップ) を使用できます。
ii) コードの局所性が向上します。コンポーネントの状態を変更する 5 つの異なるイベント ハンドラーがある場合、それらのオブザーバブルをマージし、代わりにマージされたオブザーバブルに 1 つのイベント ハンドラーを定義して、5 つのイベント ハンドラーを効果的に 1 つに結合することができます。システム全体のどのイベントがコンポーネントに影響を与えるかを簡単に判断できます。これは、すべてのイベントが 1 つのハンドラーに存在するためです。
Iterable は、遅延して消費されるシーケンスです。各アイテムは、使用するたびにイテレーターによってプルされるため、列挙は消費者によって駆動されます。
オブザーバブルは、遅延生成されたシーケンスです。各アイテムは、シーケンスに追加されるたびにオブザーバーにプッシュされるため、列挙はプロデューサーによって駆動されます。
Conal Elliott による論文「単に効率的な機能的反応性」 (直接 PDF、233 KB) は、かなり良い入門書です。対応するライブラリも機能します。
この論文は現在、別の論文、Push-pull Functional Reactive Programming (直接 PDF、286 KB) に取って代わられています。
おい、これはめちゃくちゃ素晴らしいアイデアだ!なぜ1998年にこれを知らなかったのですか?とにかく、これがフランのチュートリアルの私の解釈です。提案は大歓迎です。これに基づいてゲーム エンジンを起動することを考えています。
import pygame
from pygame.surface import Surface
from pygame.sprite import Sprite, Group
from pygame.locals import *
from time import time as epoch_delta
from math import sin, pi
from copy import copy
pygame.init()
screen = pygame.display.set_mode((600,400))
pygame.display.set_caption('Functional Reactive System Demo')
class Time:
def __float__(self):
return epoch_delta()
time = Time()
class Function:
def __init__(self, var, func, phase = 0., scale = 1., offset = 0.):
self.var = var
self.func = func
self.phase = phase
self.scale = scale
self.offset = offset
def copy(self):
return copy(self)
def __float__(self):
return self.func(float(self.var) + float(self.phase)) * float(self.scale) + float(self.offset)
def __int__(self):
return int(float(self))
def __add__(self, n):
result = self.copy()
result.offset += n
return result
def __mul__(self, n):
result = self.copy()
result.scale += n
return result
def __inv__(self):
result = self.copy()
result.scale *= -1.
return result
def __abs__(self):
return Function(self, abs)
def FuncTime(func, phase = 0., scale = 1., offset = 0.):
global time
return Function(time, func, phase, scale, offset)
def SinTime(phase = 0., scale = 1., offset = 0.):
return FuncTime(sin, phase, scale, offset)
sin_time = SinTime()
def CosTime(phase = 0., scale = 1., offset = 0.):
phase += pi / 2.
return SinTime(phase, scale, offset)
cos_time = CosTime()
class Circle:
def __init__(self, x, y, radius):
self.x = x
self.y = y
self.radius = radius
@property
def size(self):
return [self.radius * 2] * 2
circle = Circle(
x = cos_time * 200 + 250,
y = abs(sin_time) * 200 + 50,
radius = 50)
class CircleView(Sprite):
def __init__(self, model, color = (255, 0, 0)):
Sprite.__init__(self)
self.color = color
self.model = model
self.image = Surface([model.radius * 2] * 2).convert_alpha()
self.rect = self.image.get_rect()
pygame.draw.ellipse(self.image, self.color, self.rect)
def update(self):
self.rect[:] = int(self.model.x), int(self.model.y), self.model.radius * 2, self.model.radius * 2
circle_view = CircleView(circle)
sprites = Group(circle_view)
running = True
while running:
for event in pygame.event.get():
if event.type == QUIT:
running = False
if event.type == KEYDOWN and event.key == K_ESCAPE:
running = False
screen.fill((0, 0, 0))
sprites.update()
sprites.draw(screen)
pygame.display.flip()
pygame.quit()
要するに、すべてのコンポーネントを数値のように扱うことができれば、システム全体を数式のように扱うことができますよね?
PaulHudakの本であるTheHaskellSchool of Expressionは、Haskellの優れた入門書であるだけでなく、FRPにもかなりの時間を費やしています。FRPの初心者の方は、FRPの仕組みを理解していただくことを強くお勧めします。
この本の新しい書き直し(2011年にリリース、2014年に更新)、The Haskell SchoolofMusicのように見えるものもあります。
これは、時間の経過に伴う (または時間を無視した) 数学的データ変換に関するものです。
コードでは、これは機能的純粋性と宣言型プログラミングを意味します。
状態のバグは、標準の命令型パラダイムでは大きな問題です。コードのさまざまなビットは、プログラム実行中のさまざまな「時間」で共有状態を変更する場合があります。これは扱いにくい。
FRP では、(宣言型プログラミングのように) データがある状態から別の状態にどのように変換され、何がそれをトリガーするかを記述します。これにより、関数は単純に入力に反応し、現在の値を使用して新しい値を作成するため、時間を無視できます。これは、状態が変換ノードのグラフ (またはツリー) に含まれており、機能的に純粋であることを意味します。
これにより、複雑さとデバッグ時間が大幅に削減されます。
数学の A=B+C とプログラムの A=B+C の違いを考えてみてください。数学では、決して変わらない関係を説明しています。プログラムでは、「今」AがB + Cであると言っています。ただし、次のコマンドは B++ である可能性があり、この場合、A は B+C と等しくありません。数学または宣言型プログラミングでは、どの時点で質問しても、A は常に B+C と等しくなります。
したがって、共有状態の複雑さを取り除き、時間の経過とともに値を変更することによって. あなたのプログラムは、推論するのがはるかに簡単です。
EventStream は、EventStream + 何らかの変換関数です。
Behavior は、メモリ内の EventStream + Some 値です。
イベントが発生すると、変換関数を実行して値が更新されます。これが生成する値は、ビヘイビア メモリに保存されます。
N 個の他の動作を変換する新しい動作を生成するために、動作を構成できます。この構成された値は、入力イベント (動作) が発生すると再計算されます。
「オブザーバーはステートレスであるため、ドラッグの例のようにステート マシンをシミュレートするために複数のオブザーバーが必要になることがよくあります。上記の変数パスなど、関係するすべてのオブザーバーがアクセスできる状態を保存する必要があります。」
引用 - オブザーバーパターンの廃止 http://infoscience.epfl.ch/record/148043/files/DeprecatingObserversTR2010.pdf
Rx、Reactive Extensions for .NET を確認してください。彼らは、IEnumerable を使用すると、基本的にストリームから「プル」していると指摘しています。IQueryable/IEnumerable に対する Linq クエリは、セットから結果を「吸い出す」セット操作です。しかし、IObservable に対して同じ演算子を使用すると、「反応する」Linq クエリを作成できます。
たとえば、次のような Linq クエリを作成できます (MyObservableSetOfMouseMovements の m から、mX<100 および mY<100 が新しい Point(mX,mY) を選択します)。
Rx 拡張機能を使用すると、それだけです。100,100 ボックスにいるときはいつでも、マウスの動きの着信ストリームに反応して描画する UI コードがあります...