Lập trình phản ứng (chức năng) là gì?


1148

Tôi đã đọc bài viết Wikipedia về lập trình phản ứng . Tôi cũng đã đọc bài viết nhỏ về lập trình phản ứng chức năng . Các mô tả khá trừu tượng.

  1. Lập trình phản ứng chức năng (FRP) có nghĩa là gì trong thực tế?
  2. Lập trình phản ứng (trái ngược với lập trình không phản ứng?) Bao gồm những gì?

Nền tảng của tôi là các ngôn ngữ bắt buộc / OO, vì vậy một lời giải thích liên quan đến mô hình này sẽ được đánh giá cao.


159
đây là một chàng trai có trí tưởng tượng tích cực và kỹ năng kể chuyện tốt đảm nhận toàn bộ sự việc. paulstovell.com/reactive-programming
hỗn loạn

39
Ai đó thực sự cần phải viết một "Lập trình phản ứng chức năng cho người giả" cho tất cả chúng ta tự động hóa ở đây. Mọi tài nguyên tôi tìm thấy, ngay cả Elm, dường như cho rằng bạn đã nhận được bằng Thạc sĩ CS trong năm năm qua. Những người am hiểu về FRP dường như đã mất hoàn toàn khả năng nhìn nhận vấn đề từ quan điểm ngây thơ, một điều quan trọng để giảng dạy, đào tạo và truyền giáo.
TechZen

26
Một phần giới thiệu FRP xuất sắc khác: Phần giới thiệu về Lập trình phản ứng mà bạn đồng nghiệp của tôi André
Jonik

5
Một trong những điều tốt nhất tôi từng thấy, Ví dụ dựa trên: gist.github.com/staltz/868e7e9bc2a7b8c1f754
Razmig

2
Tôi thấy sự tương tự của bảng tính rất hữu ích như một ấn tượng sơ bộ đầu tiên (xem câu trả lời của Bob: stackoverflow.com/a/1033066/1593924 ). Một ô bảng tính phản ứng với những thay đổi trong các ô khác (kéo) nhưng không vươn ra và thay đổi các ô khác (không đẩy). Kết quả cuối cùng là bạn có thể thay đổi một ô và một triệu người khác 'độc lập' cập nhật màn hình của riêng họ.
Jon Coombs

Câu trả lời:


931

Nếu bạn muốn cảm nhận về FRP, bạn có thể bắt đầu với hướng dẫn cũ của Fran từ năm 1998, có hình minh họa hoạt hình. Đối với giấy tờ, hãy bắt đầu với Hoạt hình phản ứng chức năng và sau đó theo dõi các liên kết trên liên kết ấn phẩm trên trang chủ của tôi và liên kết FRP trên wiki Haskell .

Cá nhân, tôi muốn nghĩ về ý nghĩa của FRP trước khi giải quyết nó có thể được thực hiện như thế nào. (Mã không có đặc tả là câu trả lời không có câu hỏi và do đó "thậm chí không sai".) Vì vậy, tôi không mô tả FRP theo thuật ngữ biểu diễn / triển khai như Thomas K thực hiện trong một câu trả lời khác (đồ thị, nút, cạnh, bắn, thực thi, Vân vân). Có nhiều kiểu triển khai có thể, nhưng không có cách triển khai nào nói FRP gì .

Tôi cộng hưởng với mô tả đơn giản của Laurence G rằng FRP nói về "các kiểu dữ liệu đại diện cho một giá trị 'theo thời gian'". Lập trình mệnh lệnh thông thường chỉ nắm bắt các giá trị động này một cách gián tiếp, thông qua trạng thái và đột biến. Lịch sử đầy đủ (quá khứ, hiện tại, tương lai) không có đại diện hạng nhất. Hơn nữa, chỉ các giá trị tiến hóa riêng biệt mới có thể được nắm bắt (một cách gián tiếp), vì mô hình mệnh lệnh là rời rạc theo thời gian. Ngược lại, FRP nắm bắt trực tiếp các giá trị phát triển này và không gặp khó khăn với các giá trị phát triển liên tục .

FRP cũng khác thường ở chỗ nó đồng thời mà không chạy theo tổ của những con chuột lý thuyết & thực dụng gây khó khăn cho sự đồng thời bắt buộc. Về mặt ngữ nghĩa, đồng thời của FRP là chi tiết , xác địnhliên tục . (Tôi đang nói về ý nghĩa, không thực hiện. Việc triển khai có thể có hoặc không liên quan đến sự tương tranh hoặc song song.) Tính quyết định ngữ nghĩa là rất quan trọng đối với lý luận, cả nghiêm ngặt và không chính thức. Trong khi sự tương tranh làm tăng thêm sự phức tạp to lớn cho việc lập trình mệnh lệnh (do sự xen kẽ không nhất định), thì thật dễ dàng trong FRP.

Vậy, FRP là gì? Bạn có thể đã tự phát minh ra nó. Bắt đầu với những ý tưởng sau:

  • Các giá trị động / phát triển (nghĩa là các giá trị "theo thời gian") là các giá trị hạng nhất trong chính chúng. Bạn có thể định nghĩa chúng và kết hợp chúng, chuyển chúng vào & ra khỏi các chức năng. Tôi gọi những điều này là "hành vi".

  • Các hành vi được xây dựng từ một vài nguyên thủy, như các hành vi và thời gian (tĩnh) không đổi (như đồng hồ), và sau đó với sự kết hợp tuần tự và song song. n hành vi được kết hợp bằng cách áp dụng hàm n-ary (trên các giá trị tĩnh), "điểm khôn ngoan", nghĩa là liên tục theo thời gian.

  • Để giải thích cho các hiện tượng rời rạc, có một loại (sự kiện) khác của "sự kiện", mỗi loại có một luồng (hữu hạn hoặc vô hạn) xảy ra. Mỗi lần xuất hiện có một thời gian và giá trị liên quan.

  • Để đưa ra từ vựng thành phần trong đó tất cả các hành vi và sự kiện có thể được xây dựng, hãy chơi với một số ví dụ. Tiếp tục giải cấu trúc thành các phần chung chung / đơn giản hơn.

  • Để bạn biết rằng bạn đang ở trên nền tảng vững chắc, hãy tạo cho toàn bộ mô hình một nền tảng thành phần, sử dụng kỹ thuật ngữ nghĩa biểu thị, điều đó chỉ có nghĩa là (a) mỗi loại có một loại "ý nghĩa" toán học đơn giản và chính xác tương ứng, và ( b) mỗi nguyên thủy và toán tử có một ý nghĩa đơn giản và chính xác như là một chức năng của ý nghĩa của các thành phần. Không bao giờ, bao giờ trộn các cân nhắc thực hiện vào quá trình khám phá của bạn. Nếu mô tả này vô nghĩa với bạn, hãy tham khảo (a) Thiết kế biểu thị với các hình thái lớp loại , (b) Lập trình phản ứng chức năng kéo đẩy (bỏ qua các bit thực hiện) và (c) trang ngữ nghĩa học ngữ nghĩa Haskell wikibooks. Coi chừng rằng ngữ nghĩa học biểu thị có hai phần, từ hai người sáng lập Christopher Strachey và Dana Scott: phần Strachey dễ dàng & hữu dụng hơn và phần Scott khó hơn và ít hữu ích hơn (đối với thiết kế phần mềm).

Nếu bạn tuân thủ những nguyên tắc này, tôi hy vọng bạn sẽ nhận được một cái gì đó ít nhiều theo tinh thần của FRP.

Tôi đã lấy những nguyên tắc này ở đâu? Trong thiết kế phần mềm, tôi luôn đặt câu hỏi tương tự: "nó có nghĩa là gì?". Ngữ nghĩa học phi nghĩa đã cho tôi một khuôn khổ chính xác cho câu hỏi này, và một khuôn khổ phù hợp với tính thẩm mỹ của tôi (không giống như ngữ nghĩa hoạt động hoặc tiên đề, cả hai đều khiến tôi không hài lòng). Vậy tôi tự hỏi hành vi là gì? Tôi sớm nhận ra rằng bản chất rời rạc theo thời gian của tính toán mệnh lệnh là chỗ ở cho một kiểu máy cụ thể , chứ không phải là một mô tả tự nhiên về chính hành vi. Mô tả chính xác đơn giản nhất về hành vi mà tôi có thể nghĩ đến chỉ đơn giản là "chức năng của thời gian (liên tục)", vì vậy đó là mô hình của tôi. Tuyệt vời, mô hình này xử lý đồng thời liên tục, xác định một cách dễ dàng và duyên dáng.

Đó là một thách thức khá lớn để thực hiện mô hình này một cách chính xác và hiệu quả, nhưng đó là một câu chuyện khác.


78
Tôi đã nhận thức được lập trình phản ứng chức năng. Nó dường như liên quan đến nghiên cứu của riêng tôi (trong đồ họa thống kê tương tác) và tôi chắc chắn nhiều ý tưởng sẽ hữu ích cho công việc của tôi. Tuy nhiên, tôi cảm thấy rất khó khăn để vượt qua ngôn ngữ - tôi có thực sự phải tìm hiểu về "ngữ nghĩa học biểu thị" và "hình thái lớp học" để hiểu những gì đang diễn ra không? Giới thiệu đối tượng chung về chủ đề này sẽ rất hữu ích.
hadley

212
@Conal: bạn biết rõ những gì bạn đang nói, nhưng ngôn ngữ của bạn cho rằng tôi có bằng tiến sĩ về toán học tính toán, điều mà tôi không biết. Tôi có một nền tảng về kỹ thuật hệ thống và hơn 20 năm kinh nghiệm với máy tính và ngôn ngữ lập trình, tôi vẫn cảm thấy phản ứng của bạn khiến tôi gặp khó khăn. Tôi thách bạn đăng lại câu trả lời của bạn bằng tiếng Anh ;-)
mindplay.dk

50
@ minplay.dk: Nhận xét của bạn không mang lại cho tôi nhiều điều để nói về những gì bạn đặc biệt không hiểu và tôi không thích đưa ra những phỏng đoán hoang dã về tập hợp tiếng Anh cụ thể mà bạn đang tìm kiếm. Tuy nhiên, tôi mời bạn nói cụ thể những khía cạnh nào trong lời giải thích của tôi ở trên bạn đang vấp ngã, để tôi và những người khác có thể giúp bạn. Chẳng hạn, có những từ cụ thể nào bạn muốn xác định hoặc khái niệm mà bạn muốn tham khảo được thêm vào không? Tôi thực sự thích cải thiện sự rõ ràng và khả năng tiếp cận bài viết của mình - mà không làm giảm nó xuống.
Conal

27
"Xác định" / "xác định" có nghĩa là có một giá trị đúng được xác định rõ. Ngược lại, hầu hết tất cả các hình thức đồng thời bắt buộc có thể đưa ra các câu trả lời khác nhau, tùy thuộc vào lịch trình hoặc liệu bạn có tìm kiếm hay không và thậm chí chúng có thể bế tắc. "Semantic" (và cụ thể hơn là "biểu thị") đề cập đến giá trị ("biểu thị") của một biểu thức hoặc biểu diễn, trái ngược với "hoạt động" (cách trả lời được tính hoặc bao nhiêu không gian và / hoặc thời gian được sử dụng bởi cái gì loại máy).
Conal

18
Tôi đồng ý với @ mindplay.dk mặc dù tôi không thể khoe khoang đã ở trong lĩnh vực này quá lâu. Mặc dù có vẻ như bạn biết những gì bạn đang nói, nhưng nó không cho tôi hiểu nhanh chóng, ngắn gọn và đơn giản về điều này là gì, vì tôi đủ hư hỏng để mong đợi vào SO. Câu trả lời này chủ yếu đưa tôi đến hàng tấn câu hỏi mới mà không thực sự trả lời câu hỏi đầu tiên của tôi. Tôi hy vọng rằng việc chia sẻ kinh nghiệm về việc vẫn còn tương đối không biết gì về lĩnh vực này có thể giúp bạn hiểu rõ hơn về sự đơn giản và ngắn gọn mà bạn thực sự cần phải có. Tôi đến từ một nền tảng tương tự như OP, btw.
Aske B.

739

Trong lập trình chức năng thuần túy, không có tác dụng phụ. Đối với nhiều loại phần mềm (ví dụ: mọi thứ có tương tác với người dùng), các tác dụng phụ đều cần thiết ở một mức độ nào đó.

Một cách để có được hiệu ứng phụ như hành vi trong khi vẫn giữ được phong cách chức năng là sử dụng lập trình phản ứng chức năng. Đây là sự kết hợp giữa lập trình chức năng và lập trình phản ứng. (Bài viết Wikipedia mà bạn liên kết đến là về phần sau.)

Ý tưởng cơ bản đằng sau lập trình phản ứng là có một số kiểu dữ liệu nhất định đại diện cho một giá trị "theo thời gian". Các tính toán liên quan đến các giá trị thay đổi theo thời gian này sẽ có các giá trị thay đổi theo thời gian.

Ví dụ: bạn có thể biểu thị tọa độ chuột dưới dạng một cặp giá trị theo thời gian. Giả sử chúng ta đã có một cái gì đó giống như (đây là mã giả):

x = <mouse-x>;
y = <mouse-y>;

Tại bất kỳ thời điểm nào, x và y sẽ có tọa độ của chuột. Không giống như lập trình không phản ứng, chúng ta chỉ cần thực hiện nhiệm vụ này một lần và các biến x và y sẽ tự động "cập nhật". Đây là lý do tại sao lập trình phản ứng và lập trình chức năng phối hợp rất tốt với nhau: lập trình phản ứng loại bỏ nhu cầu biến đổi các biến trong khi vẫn cho phép bạn thực hiện nhiều điều bạn có thể thực hiện với các đột biến biến.

Nếu sau đó chúng tôi thực hiện một số tính toán dựa trên điều này, các giá trị kết quả cũng sẽ là các giá trị thay đổi theo thời gian. Ví dụ:

minX = x - 16;
minY = y - 16;
maxX = x + 16;
maxY = y + 16;

Trong ví dụ này, minXsẽ luôn nhỏ hơn 16 tọa độ x của con trỏ chuột. Với các thư viện nhận biết phản ứng, sau đó bạn có thể nói điều gì đó như:

rectangle(minX, minY, maxX, maxY)

Và một hộp 32x32 sẽ được vẽ xung quanh con trỏ chuột và sẽ theo dõi nó bất cứ nơi nào nó di chuyển.

Đây là một bài báo khá tốt về lập trình phản ứng chức năng .


25
Vậy lập trình phản ứng là một hình thức lập trình khai báo thì sao?
troelskn

31
> Vậy lập trình phản ứng là một dạng lập trình khai báo thì sao? Lập trình phản ứng chức năng là một hình thức lập trình chức năng, là một hình thức lập trình khai báo.
Conal

7
@ user712092 Không thực sự, không. Ví dụ: nếu tôi gọi sqrt(x)C bằng macro của bạn, điều đó chỉ cần tính toán sqrt(mouse_x())và trả lại cho tôi gấp đôi. Trong một hệ thống phản ứng chức năng thực sự, sqrt(x)sẽ trả lại một "gấp đôi theo thời gian" mới. Nếu bạn cố gắng mô phỏng một hệ thống FR với #definebạn, bạn sẽ phải thề với các biến có lợi cho các macro. Các hệ thống FR thường sẽ chỉ tính toán lại các công cụ khi cần tính toán lại, trong khi sử dụng macro có nghĩa là bạn sẽ liên tục đánh giá lại mọi thứ, tất cả các cách tiếp cận.
Laurence Gonsalves

4
"Đối với nhiều loại phần mềm (ví dụ: mọi thứ có tương tác với người dùng), các tác dụng phụ đều cần thiết ở một mức độ nào đó." Và có lẽ chỉ ở cấp độ thực hiện. Có rất nhiều tác dụng phụ trong việc thực hiện lập trình chức năng thuần túy, lười biếng, và một trong những thành công của mô hình là loại bỏ nhiều hiệu ứng đó ra khỏi mô hình lập trình. Việc tôi chuyển sang các giao diện người dùng chức năng cho thấy rằng chúng cũng có thể được lập trình hoàn toàn mà không có tác dụng phụ.
Conal

4
@tieTYT x không bao giờ được gán lại / đột biến. Giá trị của x là chuỗi các giá trị theo thời gian. Một cách khác để xem xét nó là thay vì x có giá trị "bình thường", như số, giá trị của x là (về mặt khái niệm) một hàm lấy thời gian làm tham số. (Đây là một chút đơn giản hóa. Bạn không thể tạo các giá trị thời gian cho phép bạn dự đoán tương lai của những thứ như vị trí chuột.)
Laurence Gonsalves

144

Một cách dễ dàng để đạt được trực giác đầu tiên về những gì nó giống như là tưởng tượng chương trình của bạn là một bảng tính và tất cả các biến của bạn là các ô. Nếu bất kỳ ô nào trong bảng tính thay đổi, thì bất kỳ ô nào đề cập đến ô đó cũng thay đổi. Nó cũng giống với FRP. Bây giờ hãy tưởng tượng rằng một số tế bào tự thay đổi (hay đúng hơn là được lấy từ thế giới bên ngoài): trong tình huống GUI, vị trí của chuột sẽ là một ví dụ tốt.

Điều đó nhất thiết phải bỏ lỡ khá nhiều. Phép ẩn dụ phá vỡ khá nhanh khi bạn thực sự sử dụng hệ thống FRP. Đối với một người, thường có những nỗ lực để mô hình hóa các sự kiện riêng biệt (ví dụ: chuột được nhấp). Tôi chỉ đặt điều này ở đây để cho bạn biết ý tưởng của nó.


3
Một ví dụ cực kỳ hấp dẫn. Thật tuyệt khi có những thứ lý thuyết, và có lẽ một số người hiểu được ý nghĩa của nó mà không cần phải xem xét một ví dụ nền tảng, nhưng tôi cần bắt đầu với những gì nó làm cho tôi, chứ không phải nó trừu tượng là gì. Điều tôi chỉ mới nhận được gần đây (từ các cuộc đàm phán Rx của Netflix!) Là RP (hoặc Rx, dù sao), tạo ra những "giá trị thay đổi" đầu tiên này và cho phép bạn suy luận về chúng hoặc viết các hàm thực hiện mọi thứ với chúng. Viết các hàm để tạo bảng tính hoặc ô, nếu bạn muốn. Và nó xử lý khi một giá trị kết thúc (biến mất) và cho phép bạn tự động dọn sạch.
Stewohn 14/03/2015

Ví dụ này nhấn mạnh sự khác biệt giữa lập trình hướng sự kiện và phương pháp tiếp cận phản ứng, trong đó bạn chỉ cần khai báo các phụ thuộc để sử dụng định tuyến thông minh.
kinjelom

131

Đối với tôi đó là khoảng 2 ý nghĩa khác nhau của biểu tượng =:

  1. Trong toán học x = sin(t)có nghĩa là, đó xtên khác nhau cho sin(t). Vì vậy, viết x + ylà điều tương tự như sin(t) + y. Lập trình phản ứng chức năng giống như toán học về mặt này: nếu bạn viết x + y, nó được tính toán với bất kỳ giá trị nào ttại thời điểm nó được sử dụng.
  2. Trong các ngôn ngữ lập trình giống như C (ngôn ngữ bắt buộc), x = sin(t)là một phép gán: nó có nghĩa là xlưu trữ giá trị sin(t) được thực hiện tại thời điểm chuyển nhượng.

5
Lời giải thích hay. Tôi nghĩ bạn cũng có thể thêm "thời gian" theo nghĩa FRP thường là "bất kỳ thay đổi nào từ đầu vào bên ngoài". Bất cứ khi nào một lực lượng bên ngoài thay đổi đầu vào của FRP, bạn đã di chuyển "thời gian" về phía trước và tính toán lại mọi thứ một lần nữa bị ảnh hưởng bởi sự thay đổi.
Didier A.

4
Trong toán học x = sin(t)có nghĩa xlà giá trị của sin(t)cho t. Nó không phải là một tên khác cho sin(t)chức năng. Nếu không nó sẽ được x(t) = sin(t).
Dmitri Zaitsev

+ Dấu Dmitri Zaitsev Dấu bằng có một số ý nghĩa trong toán học. Một trong số đó là bất cứ khi nào bạn nhìn thấy bên trái Bạn có thể trao đổi nó với bên phải. Ví dụ 2 + 3 = 5hay a**2 + b**2 = c**2.
dùng712092

71

OK, từ kiến ​​thức cơ bản và từ việc đọc trang Wikipedia mà bạn đã chỉ ra, có vẻ như lập trình phản ứng giống như tính toán dataflow nhưng với các "kích thích" bên ngoài cụ thể kích hoạt một tập hợp các nút để kích hoạt và thực hiện các tính toán của chúng.

Điều này khá phù hợp với thiết kế UI, ví dụ, trong đó chạm vào điều khiển giao diện người dùng (giả sử, điều khiển âm lượng trên ứng dụng phát nhạc) có thể cần cập nhật các mục hiển thị khác nhau và âm lượng thực của đầu ra âm thanh. Khi bạn sửa đổi âm lượng (một thanh trượt, giả sử) sẽ tương ứng với sửa đổi giá trị được liên kết với một nút trong biểu đồ có hướng.

Các nút khác nhau có các cạnh từ nút "giá trị âm lượng" đó sẽ tự động được kích hoạt và mọi tính toán và cập nhật cần thiết sẽ tự nhiên gợn qua ứng dụng. Ứng dụng "phản ứng" với kích thích người dùng. Lập trình phản ứng chức năng sẽ chỉ là việc thực hiện ý tưởng này bằng ngôn ngữ chức năng, hoặc nói chung trong mô hình lập trình chức năng.

Để biết thêm về "tính toán dữ liệu", hãy tìm kiếm hai từ đó trên Wikipedia hoặc sử dụng công cụ tìm kiếm yêu thích của bạn. Ý tưởng chung là thế này: chương trình là một đồ thị có hướng của các nút, mỗi nút thực hiện một số tính toán đơn giản. Các nút này được kết nối với nhau bằng các liên kết biểu đồ cung cấp đầu ra của một số nút cho đầu vào của các nút khác.

Khi một nút kích hoạt hoặc thực hiện tính toán của nó, các nút được kết nối với đầu ra của nó có đầu vào tương ứng "được kích hoạt" hoặc "được đánh dấu". Bất kỳ nút nào có tất cả các đầu vào được kích hoạt / đánh dấu / có sẵn đều tự động kích hoạt. Biểu đồ có thể ẩn hoặc rõ ràng tùy thuộc vào chính xác cách thức lập trình phản ứng được thực hiện.

Các nút có thể được xem như bắn song song, nhưng thường chúng được thực hiện theo kiểu thanh thản hoặc với sự song song hạn chế (ví dụ, có thể có một vài luồng thực thi chúng). Một ví dụ nổi tiếng là Manchester Dataflow Machine , (IIRC) đã sử dụng kiến ​​trúc dữ liệu được gắn thẻ để lên lịch thực hiện các nút trong biểu đồ thông qua một hoặc nhiều đơn vị thực thi. Điện toán dữ liệu khá phù hợp với các tình huống trong đó kích hoạt tính toán không đồng bộ làm phát sinh các tầng tính toán hoạt động tốt hơn so với việc cố gắng thực hiện được điều chỉnh bởi đồng hồ (hoặc đồng hồ).

Lập trình phản ứng nhập ý tưởng "tầng thực thi" này và dường như nghĩ về chương trình theo kiểu dữ liệu nhưng với điều kiện là một số nút được nối với "thế giới bên ngoài" và các tầng thực thi được kích hoạt khi các giác quan này được kích hoạt khi các giác quan này được kích hoạt khi các giác quan này các nút giống như thay đổi. Việc thực hiện chương trình sau đó sẽ trông giống như một cái gì đó tương tự như một cung phản xạ phức tạp. Chương trình có thể hoặc không cơ bản là không ổn định giữa các kích thích hoặc có thể chuyển sang trạng thái cơ bản giữa các kích thích.

Lập trình "không phản ứng" sẽ được lập trình với một cái nhìn rất khác về luồng thực hiện và mối quan hệ với các đầu vào bên ngoài. Có vẻ như nó hơi chủ quan, vì mọi người có thể sẽ muốn nói bất cứ điều gì phản ứng với các đầu vào bên ngoài "phản ứng" với họ. Nhưng nhìn vào tinh thần của sự việc, một chương trình thăm dò hàng đợi sự kiện ở một khoảng thời gian cố định và gửi bất kỳ sự kiện nào được tìm thấy cho các hàm (hoặc luồng) ít phản ứng hơn (vì nó chỉ liên quan đến đầu vào của người dùng ở một khoảng thời gian cố định). Một lần nữa, đó là tinh thần của vấn đề ở đây: người ta có thể tưởng tượng việc thực hiện bỏ phiếu với khoảng thời gian bỏ phiếu nhanh vào một hệ thống ở mức rất thấp và lập trình theo kiểu phản ứng lên trên nó.


1
OK, có một số câu trả lời tốt ở trên bây giờ. Tôi có nên xóa bài viết của mình? Nếu tôi thấy hai hoặc ba người nói rằng nó không thêm gì, tôi sẽ xóa nó trừ khi số lượng hữu ích của nó tăng lên. Không có điểm nào để nó ở đây trừ khi nó thêm một cái gì đó có giá trị.
Thomas Kammeyer

3
bạn đã đề cập đến luồng dữ liệu, do đó thêm một số giá trị IMHO.
Rainer Joswig

Đó là ý nghĩa của QML, dường như;)
mlvljr

3
Đối với tôi, câu trả lời này là dễ hiểu nhất, đặc biệt là vì việc sử dụng các chất tương tự tự nhiên như "gợn qua ứng dụng" và "các nút giống như cảm giác". Tuyệt quá!
Akseli Palén 15/2/2015

1
Thật không may, liên kết Manchester Dataflow Machine đã chết.
Pac0

65

Sau khi đọc nhiều trang về FRP, cuối cùng tôi cũng bắt gặp bài viết khai sáng về FRP này, cuối cùng nó cũng khiến tôi hiểu FRP thực sự là gì.

Tôi trích dẫn bên dưới Heinrich Apfelmus (tác giả của chuối phản ứng).

Bản chất của lập trình phản ứng chức năng là gì?

Một câu trả lời chung sẽ là rằng FRP FRP là tất cả về việc mô tả một hệ thống theo các chức năng thay đổi theo thời gian thay vì trạng thái biến đổi, và điều đó chắc chắn sẽ không sai. Đây là quan điểm ngữ nghĩa. Nhưng theo tôi, câu trả lời sâu sắc hơn, thỏa mãn hơn được đưa ra bởi tiêu chí cú pháp thuần túy sau đây:

Bản chất của lập trình phản ứng chức năng là xác định hành vi động của một giá trị hoàn toàn tại thời điểm khai báo.

Chẳng hạn, lấy ví dụ về một bộ đếm: bạn có hai nút được dán nhãn là Up Up và và Down Down có thể được sử dụng để tăng hoặc giảm bộ đếm. Về cơ bản, trước tiên bạn sẽ chỉ định một giá trị ban đầu và sau đó thay đổi nó bất cứ khi nào nhấn nút; đại loại như thế này:

counter := 0                               -- initial value
on buttonUp   = (counter := counter + 1)   -- change it later
on buttonDown = (counter := counter - 1)

Vấn đề là tại thời điểm khai báo, chỉ có giá trị ban đầu cho bộ đếm được chỉ định; hành vi động của bộ đếm được ẩn trong phần còn lại của văn bản chương trình. Ngược lại, lập trình phản ứng chức năng chỉ định toàn bộ hành vi động tại thời điểm khai báo, như sau:

counter :: Behavior Int
counter = accumulate ($) 0
            (fmap (+1) eventUp
             `union` fmap (subtract 1) eventDown)

Bất cứ khi nào bạn muốn hiểu động lực của bộ đếm, bạn chỉ cần nhìn vào định nghĩa của nó. Mọi thứ có thể xảy ra với nó sẽ xuất hiện ở phía bên tay phải. Điều này trái ngược hoàn toàn với cách tiếp cận bắt buộc trong đó các khai báo tiếp theo có thể thay đổi hành vi động của các giá trị được khai báo trước đó.

Vì vậy, theo tôi hiểu, một chương trình FRP là một bộ phương trình: nhập mô tả hình ảnh ở đây

j là rời rạc: 1,2,3,4 ...

fphụ thuộc vào tđiều này kết hợp khả năng mô hình hóa các kích thích bên ngoài

tất cả trạng thái của chương trình được gói gọn trong các biến x_i

Thư viện FRP sẽ chăm sóc của tiến triển thời gian, nói cách khác, dùng jđể j+1.

Tôi giải thích các phương trình này chi tiết hơn nhiều trong video này .

BIÊN TẬP:

Khoảng 2 năm sau câu trả lời ban đầu, gần đây tôi đã đi đến kết luận rằng việc triển khai FRP có một khía cạnh quan trọng khác. Họ cần (và thường làm) giải quyết một vấn đề thực tế quan trọng: vô hiệu hóa bộ đệm .

Các phương trình cho x_i-s mô tả một biểu đồ phụ thuộc. Khi một số x_ithay đổi tại thời điểm jsau đó không phải tất cả các x_i'giá trị khác j+1cần được cập nhật, do đó, không phải tất cả các phụ thuộc cần phải được tính toán lại vì một số x_i'có thể độc lập với x_i.

Hơn nữa, x_i-s mà thay đổi có thể được cập nhật tăng dần. Ví dụ: hãy xem xét một hoạt động bản đồ f=g.map(_+1)trong Scala, nơi fgListcủa Ints. Ở đây ftương ứng với x_i(t_j)gx_j(t_j). Bây giờ nếu tôi thêm một phần tử vào gthì sẽ rất lãng phí khi thực hiện mapthao tác cho tất cả các phần tử trong đó g. Một số triển khai FRP (ví dụ phản xạ-frp ) nhằm giải quyết vấn đề này. Vấn đề này còn được gọi là tính toán gia tăng.

Nói cách khác, các hành vi ( x_i-s) trong FRP có thể được coi là tính toán bộ đệm. Nhiệm vụ của công cụ FRP là vô hiệu hóa một cách hiệu quả và tính toán lại các bộ đệm (s-s x_i) này nếu một số f_i-s thay đổi.


4
Tôi đã ở ngay đó với bạn cho đến khi bạn đi với các phương trình rời rạc . Ý tưởng sáng lập của FRP là thời gian liên tục , nơi không có " j+1". Thay vào đó, hãy nghĩ về các chức năng của thời gian liên tục. Như Newton, Leibniz và những người khác đã cho chúng ta thấy, nó thường rất tiện dụng (và "tự nhiên" theo nghĩa đen) để mô tả các chức năng này một cách khác biệt, nhưng liên tục như vậy, bằng cách sử dụng các tích hợp và hệ thống ODE. Mặt khác, bạn đang mô tả một thuật toán gần đúng (và một thuật toán kém) thay vì chính nó.
Conal

Giao diện ràng buộc và bố cục ngôn ngữ HTML layx dường như thể hiện các yếu tố của FRP.

@Conal điều này khiến tôi tự hỏi FRP khác với ODE như thế nào. Chúng khác nhau như thế nào?
jhegedus

@jhegedus Trong sự tích hợp đó (có thể là đệ quy, tức là ODE) cung cấp một trong các khối xây dựng của FRP, chứ không phải toàn bộ. Mọi yếu tố của từ vựng FRP (bao gồm nhưng không giới hạn trong tích hợp) được giải thích chính xác theo thời gian liên tục. Liệu lời giải thích đó có giúp ích?
Conal


29

Tuyên bố miễn trừ trách nhiệm: câu trả lời của tôi nằm trong ngữ cảnh của rx.js - thư viện 'lập trình phản ứng' cho Javascript.

Trong lập trình chức năng, thay vì lặp qua từng mục của bộ sưu tập, bạn áp dụng các hàm bậc cao hơn (HoFs) cho chính bộ sưu tập. Vì vậy, ý tưởng đằng sau FRP là thay vì xử lý từng sự kiện riêng lẻ, hãy tạo một luồng sự kiện (được triển khai với một * có thể quan sát được) và thay vào đó áp dụng HoFs cho điều đó. Bằng cách này, bạn có thể hình dung hệ thống dưới dạng đường ống dữ liệu kết nối nhà xuất bản với người đăng ký.

Những ưu điểm chính của việc sử dụng một thiết bị quan sát được là:
i) nó trừu tượng hóa trạng thái khỏi mã của bạn, ví dụ: nếu bạn muốn trình xử lý sự kiện chỉ bị sa thải cho mỗi sự kiện thứ nhất hoặc ngừng bắn sau các sự kiện đầu tiên, hoặc chỉ bắt đầu bắn sau các sự kiện đầu tiên, bạn chỉ có thể sử dụng HoFs (bộ lọc, TakeUntil, bỏ qua tương ứng) thay vì cài đặt, cập nhật và kiểm tra bộ đếm.
ii) nó cải thiện tính cục bộ mã - nếu bạn có 5 trình xử lý sự kiện khác nhau thay đổi trạng thái của một thành phần, bạn có thể hợp nhất các quan sát của chúng và xác định một trình xử lý sự kiện duy nhất trên có thể quan sát được thay vào đó, kết hợp hiệu quả 5 trình xử lý sự kiện thành 1. Điều này làm cho nó rất dễ dàng lý luận về những sự kiện trong toàn bộ hệ thống của bạn có thể ảnh hưởng đến một thành phần, vì tất cả đều có trong một trình xử lý.

  • Một quan sát là kép của một Iterable.

Một Iterable là một chuỗi tiêu thụ lười biếng - mỗi mục được kéo bởi trình lặp bất cứ khi nào nó muốn sử dụng nó, và do đó phép liệt kê được điều khiển bởi người tiêu dùng.

Một quan sát là một chuỗi được sản xuất một cách lười biếng - mỗi mục được đẩy đến người quan sát bất cứ khi nào nó được thêm vào trình tự, và do đó phép liệt kê được điều khiển bởi nhà sản xuất.


1
Cảm ơn bạn rất nhiều vì định nghĩa đơn giản này về một quan sát và sự khác biệt của nó với iterables. Tôi nghĩ thường rất hữu ích khi so sánh một khái niệm phức tạp với khái niệm kép nổi tiếng của nó để có được sự hiểu biết thực sự.

2
"Vì vậy, ý tưởng đằng sau FRP là thay vì xử lý từng sự kiện riêng lẻ, hãy tạo một luồng sự kiện (được triển khai với một * có thể quan sát được) và thay vào đó áp dụng HoFs cho điều đó." Tôi có thể nhầm, nhưng tôi tin rằng đây không thực sự là FRP mà là một sự trừu tượng tốt đẹp đối với mẫu thiết kế Observer cho phép các hoạt động chức năng thông qua HoF (rất tuyệt!) Trong khi vẫn được sử dụng với mã bắt buộc. Thảo luận về chủ đề - lambda-the-ultimate.org/node/4982
nqe

18

Anh bạn, đây là một ý tưởng tuyệt vời! Tại sao tôi không tìm hiểu về điều này vào năm 1998? Dù sao, đây là cách giải thích của tôi về hướng dẫn của Fran . Đề xuất được chào đón nhất, tôi đang nghĩ về việc bắt đầu một công cụ trò chơi dựa trên điều này.

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()

Tóm lại: Nếu mọi thành phần có thể được coi như một số, toàn bộ hệ thống có thể được xử lý như một phương trình toán học, phải không?


1
Điều này hơi muộn, nhưng dù sao thì ... Frag là một trò chơi sử dụng FRP .
arx

14

Cuốn sách của Paul Hudak, The Haskell School of Expression , không chỉ là một lời giới thiệu hay về Haskell, mà nó còn dành một lượng thời gian hợp lý cho FRP. Nếu bạn là người mới bắt đầu với FRP, tôi thực sự khuyên bạn nên cung cấp cho bạn cảm giác về cách FRP hoạt động.

Ngoài ra còn có những gì trông giống như một bản viết lại mới của cuốn sách này (phát hành năm 2011, cập nhật 2014), Trường âm nhạc Haskell .


10

Theo các câu trả lời trước đó, dường như về mặt toán học, chúng ta chỉ đơn giản nghĩ theo thứ tự cao hơn. Thay vì nghĩ một giá trị x có loại X , chúng ta nghĩ về hàm x : TX , trong đó T là loại thời gian, có thể là số tự nhiên, số nguyên hoặc liên tục. Bây giờ khi chúng ta viết y : = x + 1 bằng ngôn ngữ lập trình, chúng ta thực sự có nghĩa là phương trình y ( t ) = x ( t ) + 1.


9

Hành vi như một bảng tính như đã lưu ý. Thông thường dựa trên một khung hướng sự kiện.

Như với tất cả các "mô hình", tính mới của nó là điều gây tranh cãi.

Từ kinh nghiệm của tôi về các mạng lưu lượng phân tán của các tác nhân, nó có thể dễ dàng trở thành vấn đề chung về tính nhất quán trạng thái trên mạng của các nút, tức là bạn kết thúc với rất nhiều dao động và bị mắc kẹt trong các vòng lặp lạ.

Điều này là khó tránh vì một số ngữ nghĩa ngụ ý các vòng lặp tham chiếu hoặc phát sóng, và có thể khá hỗn loạn khi mạng lưới các diễn viên hội tụ (hoặc không) trên một số trạng thái không thể đoán trước.

Tương tự, một số tiểu bang có thể không đạt được, mặc dù có các cạnh được xác định rõ, bởi vì nhà nước toàn cầu tránh xa giải pháp. 2 + 2 có thể hoặc không thể là 4 tùy thuộc vào thời điểm 2 trở thành 2 và liệu họ có giữ nguyên như vậy không. Bảng tính có đồng hồ và phát hiện vòng lặp đồng bộ. Diễn viên phân phối thường không.

Chúc mọi người vui vẻ :).



7

Bài viết này của Andre Staltz là lời giải thích tốt nhất và rõ ràng nhất tôi từng thấy cho đến nay.

Một số trích dẫn từ bài viết:

Lập trình phản ứng là lập trình với các luồng dữ liệu không đồng bộ.

Trên hết, bạn được cung cấp một hộp công cụ tuyệt vời để kết hợp, tạo và lọc bất kỳ luồng nào trong số đó.

Đây là một ví dụ về các sơ đồ tuyệt vời là một phần của bài viết:

Nhấp vào sơ đồ luồng sự kiện


5

Đó là về biến đổi dữ liệu toán học theo thời gian (hoặc bỏ qua thời gian).

Trong mã này có nghĩa là độ tinh khiết chức năng và lập trình khai báo.

Lỗi nhà nước là một vấn đề lớn trong mô hình mệnh lệnh tiêu chuẩn. Các bit mã khác nhau có thể thay đổi một số trạng thái chia sẻ tại các "thời điểm" khác nhau trong quá trình thực thi chương trình. Điều này là khó đối phó.

Trong FRP bạn mô tả (như trong lập trình khai báo) cách dữ liệu biến đổi từ trạng thái này sang trạng thái khác và điều gì kích hoạt nó. Điều này cho phép bạn bỏ qua thời gian vì chức năng của bạn chỉ đơn giản là phản ứng với các đầu vào của nó và sử dụng các giá trị hiện tại của chúng để tạo một giá trị mới. Điều này có nghĩa là trạng thái được chứa trong biểu đồ (hoặc cây) của các nút biến đổi và hoàn toàn có chức năng.

Điều này ồ ạt làm giảm độ phức tạp và thời gian gỡ lỗi.

Hãy nghĩ về sự khác biệt giữa A = B + C trong toán học và A = B + C trong một chương trình. Trong toán học, bạn đang mô tả một mối quan hệ sẽ không bao giờ thay đổi. Trong một chương trình, nó nói rằng "Ngay bây giờ" A là B + C. Nhưng lệnh tiếp theo có thể là B ++ trong trường hợp A không bằng B + C. Trong toán học hoặc lập trình khai báo A sẽ luôn bằng B + C bất kể thời điểm nào bạn yêu cầu.

Vì vậy, bằng cách loại bỏ sự phức tạp của trạng thái chia sẻ và thay đổi giá trị theo thời gian. Chương trình của bạn dễ dàng hơn nhiều để lý do về.

EventStream là EventStream + một số chức năng chuyển đổi.

Hành vi là một EventStream + Một số giá trị trong bộ nhớ.

Khi sự kiện kích hoạt giá trị được cập nhật bằng cách chạy chức năng biến đổi. Giá trị mà cái này tạo ra được lưu trữ trong bộ nhớ hành vi.

Các hành vi có thể được sáng tác để tạo ra các hành vi mới là sự biến đổi trên N các hành vi khác. Giá trị cấu thành này sẽ tính toán lại khi các sự kiện (hành vi) đầu vào cháy.

"Vì các nhà quan sát là không trạng thái, chúng tôi thường cần một vài trong số họ để mô phỏng một máy trạng thái như trong ví dụ kéo. Chúng tôi phải lưu trạng thái nơi tất cả các nhà quan sát có thể truy cập được như trong đường dẫn biến đổi ở trên."

Trích dẫn từ - Không tán thành Mẫu Người quan sát http://infoscience.epfl.ch/record/148043/files/DeprecatingObserversTR2010.pdf


Đây chính xác là cách tôi cảm nhận về lập trình khai báo, và bạn chỉ mô tả ý tưởng tốt hơn tôi.
neevek

2

Lời giải thích ngắn gọn và rõ ràng về Lập trình Phản ứng xuất hiện trên Chu kỳ - Lập trình Phản ứng , nó sử dụng các mẫu đơn giản và trực quan.

[Mô-đun / Thành phần / đối tượng] là phản ứng có nghĩa là nó hoàn toàn chịu trách nhiệm quản lý trạng thái của chính nó bằng cách phản ứng với các sự kiện bên ngoài.

Lợi ích của phương pháp này là gì? Đó là Inversion of Control , chủ yếu vì [mô-đun / Thành phần / đối tượng] tự chịu trách nhiệm, cải thiện việc đóng gói bằng các phương thức riêng chống lại các phương thức công khai.

Đó là một điểm khởi đầu tốt, không phải là một nguồn kiến ​​thức đầy đủ. Từ đó bạn có thể chuyển sang các giấy tờ phức tạp và sâu sắc hơn.


0

Kiểm tra Rx, phần mở rộng Reactive cho .NET. Họ chỉ ra rằng với IEnumerable, bạn về cơ bản là 'kéo' từ một luồng. Các truy vấn Linq trên IQueryable / IEnumerable là các hoạt động được thiết lập để "hút" các kết quả ra khỏi một tập hợp. Nhưng với các toán tử tương tự trên IObservable, bạn có thể viết các truy vấn Linq 'phản ứng'.

Ví dụ: bạn có thể viết một truy vấn Linq như (từ m trong MyObservableSetOfMouseMovements trong đó mX <100 và mY <100 chọn Điểm mới (mX, mY)).

và với các tiện ích mở rộng Rx, đó là: Bạn có mã UI phản ứng với luồng di chuyển chuột đến và rút ra bất cứ khi nào bạn ở trong hộp 100.100 ...


0

FRP là sự kết hợp giữa lập trình chức năng (mô hình lập trình được xây dựng dựa trên ý tưởng của mọi thứ là một chức năng) và mô hình lập trình phản ứng (được xây dựng dựa trên ý tưởng rằng mọi thứ đều là một luồng (triết lý quan sát và quan sát được)). Nó được cho là tốt nhất của thế giới.

Kiểm tra bài viết của Andre Staltz về lập trình phản ứng để bắt đầu.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.