Programowanie asynchroniczne w językach funkcjonalnych

31

Jestem głównie programistą C / C ++, co oznacza, że ​​większość mojego doświadczenia dotyczy paradygmatów proceduralnych i obiektowych. Jednak, jak wielu programistów C ++ zdaje sobie sprawę, C ++ z biegiem lat przesunął nacisk na styl funkcjonalny, którego kulminacją było wreszcie dodanie lambdas i zamknięć w C ++ 0x.

Niezależnie od tego, chociaż mam duże doświadczenie w kodowaniu w funkcjonalnym stylu za pomocą C ++, mam bardzo małe doświadczenie z rzeczywistymi językami funkcjonalnymi, takimi jak Lisp, Haskell itp.

Niedawno zacząłem studiować te języki, ponieważ idea „braku efektów ubocznych” w czysto funkcjonalnych językach zawsze mnie intrygowała, szczególnie w odniesieniu do jej zastosowań do współbieżności i przetwarzania rozproszonego.

Jednak, pochodząc ze środowiska C ++, nie jestem pewien, jak ta filozofia „bez efektów ubocznych” działa z programowaniem asynchronicznym. Przez programowanie asynchroniczne rozumiem każdy styl frameworku / API / kodowania, który rozsyła procedury obsługi zdarzeń dostarczone przez użytkownika do obsługi zdarzeń, które występują asynchronicznie (poza przepływem programu). Obejmuje to biblioteki asynchroniczne, takie jak Boost.ASIO, a nawet zwykły stary C procedury obsługi sygnałów lub procedury obsługi zdarzeń GUI Java.

Jedną z nich wszystkich jest to, że natura programowania asynchronicznego wymaga tworzenia efektów ubocznych (stanu), aby główny przepływ programu mógł się dowiedzieć, że wywołano asynchroniczną procedurę obsługi zdarzeń. Zazwyczaj w środowisku takim jak Boost.ASIO procedura obsługi zdarzeń zmienia stan obiektu, dzięki czemu efekt zdarzenia jest propagowany poza okresem istnienia funkcji obsługi zdarzeń. Naprawdę, co jeszcze może zrobić moduł obsługi zdarzeń? Nie może „zwrócić” wartości do punktu wywoławczego, ponieważ nie ma punktu wywoławczego. Program obsługi zdarzeń nie jest częścią głównego przepływu programu, więc jedynym sposobem, w jaki może on mieć jakikolwiek wpływ na rzeczywisty program, jest zmiana pewnego stanu (lub innego longjmppunktu wykonania).

Wygląda więc na to, że programowanie asynchroniczne polega na asynchronicznym wytwarzaniu efektów ubocznych. Wydaje się to całkowicie sprzeczne z celami programowania funkcjonalnego. Jak pogodzić te dwa paradygmaty (w praktyce) w językach funkcjonalnych?

Charles Salvia
źródło
3
Wow, właśnie miałem napisać takie pytanie i nie wiedziałem, jak je umieścić, a potem zobaczyłem to w sugestiach!
Amogh Talpallikar

Odpowiedzi:

11

Cała logika jest zdrowa, z wyjątkiem tego, że myślę, że rozumienie programowania funkcjonalnego jest nieco zbyt ekstremalne. W prawdziwym świecie programowanie funkcjonalne, podobnie jak programowanie obiektowe lub programowanie imperatywne, dotyczy sposobu myślenia i podejścia do problemu. Nadal możesz pisać programy w duchu programowania funkcjonalnego, modyfikując jednocześnie stan aplikacji.

W rzeczywistości musisz zmodyfikować stan aplikacji, aby cokolwiek zrobić . Chłopaki z Haskell powiedzą ci, że ich programy są „czyste”, ponieważ zawijają wszystkie zmiany stanu w monadzie. Jednak ich programy nadal współdziałają ze światem zewnętrznym. (W przeciwnym razie o co chodzi!)

Funkcjonalne programowanie kładzie nacisk na „brak efektów ubocznych”, gdy ma to sens. Jednak aby programować w świecie rzeczywistym, jak powiedziałeś, musisz zmodyfikować jego stan. (Na przykład reagowanie na zdarzenia, zapisywanie na dysku itd.)

Aby uzyskać więcej informacji na temat programowania asynchronicznego w językach funkcjonalnych, zdecydowanie zachęcam do zapoznania się z modelem programowania asynchronicznego przepływu pracy F # . Pozwala pisać programy funkcjonalne, ukrywając wszystkie nieporządne szczegóły przejścia wątku w bibliotece. (W sposób bardzo podobny do monad w stylu Haskell.)

Jeśli „ciało” wątku po prostu oblicza wartość, wówczas odrodzenie wielu wątków i równoległe obliczenie ich wartości pozostaje nadal w paradygmacie funkcjonalnym.

Chris Smith
źródło
5
Ponadto: pomaga spojrzeć na Erlanga. Język jest bardzo prosty, czysty (wszystkie dane są niezmienne), a to wszystko o asynchronicznego przetwarzania.
9000
Zasadniczo po zrozumieniu zalet podejścia funkcjonalnego i zmianie stanu tylko wtedy, gdy ma to znaczenie, automatycznie upewni się, że nawet jeśli pracujesz w powiedzeniu czegoś takiego jak Java, będziesz wiedział, kiedy zmodyfikować stan i jak kontrolować takie rzeczy.
Amogh Talpallikar
Nie zgadzam się - fakt, że program składa się z „czystych” funkcji, nie oznacza, że ​​nie działa on ze światem zewnętrznym, oznacza to, że każda funkcja w programie dla jednego zestawu argumentów zawsze zwróci ten sam wynik, i to jest (czystość) to wielka sprawa, ponieważ z praktycznego punktu widzenia - taki program będzie mniej wadliwy, bardziej „testowalny”, udane wykonanie funkcji można by udowodnić matematycznie.
Gill Bates,
8

To fascynujące pytanie. Moim zdaniem najciekawszym podejściem jest podejście przyjęte w Clojure i wyjaśnione w tym filmie:

http://www.infoq.com/presentations/Value-Identity-State-Rich-Hickey

Zasadniczo proponowane „rozwiązanie” jest następujące:

  • Większość kodu piszesz jako klasyczne „czyste” funkcje z niezmiennymi strukturami danych i bez efektów ubocznych
  • Efekty uboczne są izolowane za pomocą zarządzanych referencji, które kontrolują zmiany zgodnie z regułami pamięci transakcyjnej oprogramowania (tj. Wszystkie aktualizacje stanu zmiennego odbywają się w ramach odpowiedniej izolowanej transakcji)
  • Jeśli weźmiesz ten widok świata, asynchroniczne „zdarzenia” będą wyzwalały transakcyjną aktualizację stanu zmiennego, w którym sama aktualizacja jest czystą funkcją.

Prawdopodobnie nie wyraziłem tego pomysłu tak jasno, jak inni, ale mam nadzieję, że to daje ogólny pomysł - w zasadzie używa współbieżnego systemu STM, aby zapewnić „pomost” między czystym programowaniem funkcjonalnym a asynchroniczną obsługą zdarzeń.

mikera
źródło
6

Jedna uwaga: funkcjonalny język jest czysty, ale jego środowisko wykonawcze nie jest.

Na przykład środowiska wykonawcze Haskell obejmują kolejki, multipleksowanie wątków, wyrzucanie elementów bezużytecznych itp., Które nie są czyste.

Dobrym przykładem jest lenistwo. Haskell obsługuje leniwe ocenianie (tak naprawdę jest to ustawienie domyślne). Tworzysz leniwą wartość, przygotowując operację, a następnie możesz utworzyć wiele kopii tej wartości, i nadal jest ona „leniwa”, o ile nie jest wymagana. Gdy wynik jest potrzebny lub jeśli środowisko wykonawcze znajdzie trochę czasu, wartość jest faktycznie obliczana, a stan leniwego obiektu zmienia się, odzwierciedlając, że nie jest już wymagane wykonywanie obliczeń (jeszcze raz), aby uzyskać wynik. Jest teraz dostępny we wszystkich odniesieniach, więc stan obiektu zmienił się, mimo że jest to czysty język.

Matthieu M.
źródło
2

Jestem zdezorientowany, jak ta filozofia „bez efektów ubocznych” działa z programowaniem asynchronicznym. Przez programowanie asynchroniczne rozumiem ...

To właśnie o to chodzi.

Dźwiękowy styl bez efektów ubocznych jest niezgodny z ramami zależnymi od stanu. Znajdź nowe ramy.

Na przykład standard WSGI w Pythonie pozwala nam tworzyć aplikacje niepożądane.

Chodzi o to, że różne „zmiany stanu” znajdują odzwierciedlenie w środowisku wartości, które można budować stopniowo. Każde żądanie jest potokiem transformacji.

S.Lott
źródło
„zezwala na tworzenie aplikacji bez efektów ubocznych” Myślę, że gdzieś tam brakuje słowa.
Christopher Mahan
1

Po nauczeniu się enkapsulacji z Borland C ++ po nauce C, kiedy Borland C ++ nie miał szablonów, które umożliwiały generyczne, paradygmat orientacji obiektowej sprawił, że poczułem się nieswojo. Nieco bardziej naturalny sposób obliczania wydawał się filtrowaniem danych przez rury. Zewnętrzny strumień miał oddzielną i niezależną tożsamość od wewnętrznego niezmiennego strumienia wejściowego, zamiast być uważany za efekt uboczny, tj. Każde źródło danych (lub filtr) było niezależne od innych. Naciśnięcie klawisza (zdarzenie przykładowe) ograniczyło asynchroniczne kombinacje danych wejściowych użytkownika do dostępnych kodów klawiszy. Funkcje działają na argumentach parametrów wejściowych, a stan enkapsulowany przez klasę jest po prostu skrótem do unikania jawnego przekazywania powtarzalnych argumentów między małym podzbiorem funkcji, poza tym jest ostrożny w ograniczonym kontekście, zapobiegając nadużywaniu tych argumentów z dowolnej funkcji.

Sztywne przestrzeganie określonego paradygmatu powoduje niedogodności związane z nieszczelnymi abstrakcjami, np. komercyjne środowiska wykonawcze, takie jak JRE, DirectX, .net. Aby ograniczyć tę niedogodność, języki albo wybierają naukowo wyrafinowane monady, jak Haskell, albo elastyczne wsparcie dla wielu paradygmatów, jak w końcu F #. O ile enkapsulacja nie jest przydatna w przypadku użycia wielokrotnego dziedziczenia, podejście oparte na wielu paradygmatach może być lepszą alternatywą dla niektórych, czasem złożonych, specyficznych dla paradygmatu wzorców programowania.

Chawathe Vipul
źródło