Co sprawia, że ​​funkcjonalne języki programowania są deklaratywne, a nie imperatywne?

17

W wielu artykułach opisujących zalety programowania funkcjonalnego widziałem funkcjonalne języki programowania, takie jak Haskell, ML, Scala lub Clojure, nazywane „językami deklaratywnymi”, różniącymi się od języków imperatywnych, takich jak C / C ++ / C # / Java. Moje pytanie brzmi: co sprawia, że ​​funkcjonalne języki programowania są deklaratywne, a nie imperatywne.

Często spotykanym wyjaśnieniem opisującym różnice między programowaniem deklaratywnym a imperatywnym jest to, że w programowaniu imperatywnym mówisz komputerowi „jak coś zrobić”, a nie „co robić” w językach deklaratywnych. Problem z tym objaśnieniem polega na tym, że ciągle robisz to we wszystkich językach programowania. Nawet jeśli zejdziesz na najniższy poziom, wciąż mówisz komputerowi „Co robić”, każesz CPU dodać dwie liczby, nie instruujesz go, jak wykonać dodawanie. Jeśli przejdziemy do drugiego końca spektrum, czysto funkcjonalnego języka wysokiego poziomu, takiego jak Haskell, w rzeczywistości mówisz komputerowi, jak osiągnąć określone zadanie, to właśnie twój program jest sekwencją instrukcji, aby osiągnąć określone zadanie, którego komputer nie wie, jak wykonać samodzielnie. Rozumiem, że języki takie jak Haskell, Clojure itp. Są oczywiście na wyższym poziomie niż C / C ++ / C # / Java i oferują takie funkcje, jak leniwa ocena, niezmienne struktury danych, anonimowe funkcje, curry, trwałe struktury danych itp. programowanie funkcjonalne możliwe i wydajne, ale nie klasyfikowałbym ich jako języków deklaratywnych.

Dla mnie językiem czysto deklaratywnym byłby ten, który składa się wyłącznie z deklaracji, przykładem takich języków byłby CSS (tak, wiem, że CSS technicznie nie byłby językiem programowania). CSS zawiera tylko deklaracje stylu, które są używane przez HTML i JavaScript strony. CSS nie może robić nic innego poza deklaracjami, nie może tworzyć funkcji klasowych, tj. Funkcji, które określają styl wyświetlania na podstawie jakiegoś parametru, nie można wykonywać skryptów CSS itp. To dla mnie opisuje język deklaratywny (zauważ, że nie powiedziałem deklaratywny język programowania ).

Aktualizacja:

Ostatnio bawiłem się z Prologiem i dla mnie Prolog jest najbliższym językiem programowania do języka w pełni deklaratywnego (przynajmniej moim zdaniem), jeśli nie jest to jedyny w pełni deklaratywny język programowania. Aby opracować programowanie w Prologu, należy wykonać deklaracje określające albo fakt (funkcja predykatu, która zwraca wartość true dla określonego wejścia), albo regułę (funkcja predykatu, która zwraca true dla danego warunku / wzorca na podstawie danych wejściowych), reguły są zdefiniowane przy użyciu techniki dopasowania wzorca. Aby cokolwiek zrobić w prologu, przeszukujesz bazę wiedzy, zastępując jedną lub więcej danych wejściowych predykatu zmienną, a prolog próbuje znaleźć wartości dla zmiennych, dla których predykat się powiedzie.

Chodzi mi o prolog, że nie ma instrukcji bezwzględnych, po prostu mówisz (deklarujesz) komputerowi, co on wie, a potem pytasz (pytasz) o wiedzę. W funkcjonalnych językach programowania nadal podajesz instrukcje, tj. Weź wartość, wywołaj funkcję X i dodaj do niej 1 itd., Nawet jeśli nie manipulujesz bezpośrednio lokalizacjami pamięci lub nie zapisujesz obliczeń krok po kroku. Nie powiedziałbym, że programowanie w Haskell, ML, Scali lub Clojure jest w tym sensie deklaratywne, chociaż mogę się mylić. Jest właściwym, prawdziwym, czysto funkcjonalnym programowaniem deklaratywnym w sensie, który opisałem powyżej.

ALXGTV
źródło
Gdzie jest @ JimmyHoffa, kiedy go potrzebujesz?
2
1. C # i współczesne C ++ to bardzo funkcjonalne języki. 2. Pomyśl o różnicy w pisaniu SQL i Java, aby osiągnąć podobny cel (może jakieś złożone zapytanie z łączeniami). możesz także pomyśleć, czym LINQ w C # różni się od pisania prostych pętli foreach ...
AK_
także różnica jest bardziej kwestią stylu niż czegoś jasno zdefiniowanego ... chyba że używasz prologu ...
AK_
1
Jednym z kluczy do rozpoznania różnicy jest użycie operatora przypisania w porównaniu z operatorem definicji / deklaracji - na przykład w Haskell nie ma operatora przypisania . Zachowanie tych dwóch operatorów jest bardzo różne i zmusza cię do zaprojektowania kodu w inny sposób.
Jimmy Hoffa
1
Niestety, nawet bez operatora przypisania mogę to zrobić (let [x 1] (let [x (+ x 2)] (let [x (* x x)] x)))( mam nadzieję, że to zrozumiałeś, Clojure). Moje oryginalne pytanie brzmi: co różni to od tego int? x = 1; x += 2; x *= x; return x;Moim zdaniem jest w większości taki sam.
ALXGTV

Odpowiedzi:

16

Wygląda na to, że rysujesz linię między deklarowaniem rzeczy a instruowaniem maszyny. Nie ma tak trudnej i szybkiej separacji. Ponieważ maszyna, której nauczono programowania imperatywnego, nie musi być sprzętem fizycznym, istnieje duża swoboda interpretacji. Prawie wszystko można postrzegać jako jawny program dla właściwej maszyny abstrakcyjnej. Na przykład CSS może być postrzegany jako język wysokiego poziomu do programowania maszyny, która przede wszystkim rozwiązuje selektory i ustawia atrybuty tak wybranych obiektów DOM.

Pytanie brzmi, czy taka perspektywa jest sensowna i odwrotnie, jak bardzo sekwencja instrukcji przypomina deklarację obliczania wyniku. W przypadku CSS perspektywa deklaratywna jest wyraźnie bardziej przydatna. W przypadku C wyraźnie dominuje perspektywa imperatywna. Jeśli chodzi o języki jak Haskell, cóż…

Język ma określoną, konkretną semantykę. Oznacza to, oczywiście, że można interpretować program jako ciąg operacji. Wybieranie prymitywnych operacji nie wymaga nawet zbyt wiele wysiłku, aby ładnie odwzorować je na sprzęcie towarowym (tak robią maszyny STG i inne modele).

Jednak sposób, w jaki pisane są programy Haskell, często można rozsądnie odczytać jako opis wyniku do obliczenia. Weźmy na przykład program do obliczenia sumy pierwszych N silni:

sum_of_fac n = sum [product [1..i] | i <- ..n]

Możesz to odczytywać i czytać jako sekwencję operacji STG, ale o wiele bardziej naturalne jest czytanie go jako opisu wyniku (który, moim zdaniem, jest bardziej użyteczną definicją programowania deklaratywnego niż „co obliczyć”): Wynikiem jest suma iloczynów [1..i]dla wszystkich i= 0,…, n. Jest to o wiele bardziej deklaratywne niż prawie jakikolwiek program lub funkcja C.

Mathias Bynens
źródło
1
Powodem, dla którego zadałem to pytanie, jest to, że wiele osób próbuje promować programowanie funkcjonalne jako nowy odrębny paradygmat całkowicie oddzielony od paradygmatów C / C ++ / C # / Java lub nawet Python, podczas gdy w rzeczywistości programowanie funkcjonalne jest po prostu inną formą wyższego poziomu programowanie imperatywne.
ALXGTV
Aby wyjaśnić, co mam na myśli tutaj, definiujesz sum_of_fac, ale sum_of_fac jest samo w sobie prawie bezużyteczne, chyba że to jest to, co robi twoja aplikacja do dziur, musisz użyć wyniku, aby obliczyć inny wynik, który ostatecznie tworzy się do osiągnięcia określonego zadania, zadanie, które twój program zaprojektował do rozwiązania.
ALXGTV
6
@ALXGTV Programowanie funkcjonalne to zupełnie inny paradygmat, nawet jeśli jesteś nieugięty w czytaniu go jako koniecznego (jest to bardzo szeroka klasyfikacja, chodzi o coś więcej niż w paradygmaty programowania). Inny kod, który opiera się na tym kodzie, może być odczytany również deklaratywnie: Na przykład map (sum_of_fac . read) (lines fileContent). Oczywiście w pewnym momencie zaczyna się we / wy, ale jak powiedziałem, jest to kontinuum. Części programów Haskell są bardziej konieczne, x + yload x; load y; add;
1
@ALXGTV Twoja intuicja jest prawidłowa: deklaratywne programowanie „po prostu” zapewnia wyższy poziom abstrakcji w stosunku do niektórych imperatywnych szczegółów. Twierdziłbym, że to wielka sprawa!
Andres F.
1
@Brendan Nie sądzę, aby programowanie funkcjonalne było podzbiorem programowania imperatywnego (ani niezmienność nie jest obowiązkową cechą FP). Można argumentować, że w przypadku czystego, statycznie typowanego FP, jest to nadzbiór programowania imperatywnego, w którym efekty są wyraźne w typach. Znasz dosadne stwierdzenie, że Haskell jest „najlepszym językiem imperatywnym na świecie”;)
Andres F.
21

Podstawową jednostką programu imperatywnego jest stwierdzenie . Instrukcje są wykonywane ze względu na ich skutki uboczne. Zmieniają stan, który otrzymują. Sekwencja instrukcji jest sekwencją poleceń, oznaczającą zrób to, a następnie zrób to. Programista określa dokładną kolejność wykonywania obliczeń. To ludzie mają na myśli, mówiąc komputerowi, jak to zrobić.

Podstawową jednostką programu deklaratywnego jest wyrażenie . Wyrażenia nie mają skutków ubocznych. Określają zależność między wejściem a wyjściem, tworząc nowe i oddzielne wyjście od swoich danych wejściowych, zamiast modyfikować ich stan wejściowy. Sekwencja wyrażeń nie ma znaczenia, a niektóre nie zawierają wyrażeń określających relacje między nimi. Programista określa relacje między danymi, a program określa kolejność wykonywania obliczeń na podstawie tych relacji. To ludzie mają na myśli, mówiąc komputerowi, co mają robić.

Języki imperatywne mają wyrażenia, ale ich głównym sposobem wykonywania zadań są stwierdzenia. Podobnie języki deklaratywne mają pewne wyrażenia z semantyką podobne do instrukcji, takie jak sekwencje monad w donotacji Haskella , ale w ich rdzeniu są jednym wielkim wyrażeniem. To pozwala początkującym pisać kod, który wygląda bardzo imperatywnie, ale prawdziwa potęga języka pojawia się, gdy uciekasz od tego paradygmatu.

Karl Bielefeldt
źródło
1
Byłaby to idealna odpowiedź, gdyby zawierała przykład. Najlepszym przykładem tego co wiem, aby zilustrować deklaratywna vs. konieczne jest LINQ vs. foreach.
Robert Harvey
7

Prawdziwą cechą definiującą, która oddziela deklaratywne od programowania imperatywnego, jest styl deklaratywny, w którym nie podaje się instrukcji sekwencyjnych; na niższym poziomie tak, procesor działa w ten sposób, ale jest to problem kompilatora.

Sugerujesz, że CSS jest „językiem deklaratywnym”, w ogóle nie nazwałbym tego językiem. Jest to format struktury danych, taki jak JSON, XML, CSV lub INI, po prostu format do definiowania danych, które są znane interpretatorowi jakiejś natury.

Interesujące skutki uboczne występują, gdy wyjmiesz operatora przypisania z języka, i to jest prawdziwa przyczyna utraty wszystkich instrukcji rozkazujących step1-step2-step3 w językach deklaratywnych.

Co robisz z operatorem przypisania w funkcji? Tworzysz kroki pośrednie. Taki jest sedno, operator przypisania służy do zmiany danych krok po kroku. Jak tylko nie możesz już zmieniać danych, tracisz wszystkie te kroki i kończysz się następującymi:

Każda funkcja ma tylko jedną instrukcję, która jest pojedynczą deklaracją

Teraz istnieje wiele sposobów, aby jedna instrukcja wyglądała jak wiele instrukcji, ale jest to na przykład sztuczka: 1 + 3 + (2*4) + 8 - (7 / (8*3))jest to wyraźnie jedna instrukcja, ale jeśli napiszesz ją jako ...

1 +
3 +
(
  2*4
) +
8 - 
(
  7 / (8*3)
)

Może przybierać postać sekwencji operacji, która może ułatwić mózgowi rozpoznanie pożądanego rozkładu, który zamierzał autor. Często robię to w C # z kodem jak ...

people
  .Where(person => person.MiddleName != null)
  .Select(person => person.MiddleName)
  .Distinct();

Jest to pojedyncze stwierdzenie, ale pokazuje wygląd rozłożonego na wiele - zauważ, że nie ma żadnych przypisań.

Zauważ również, w obu powyższych metodach - sposób, w jaki kod jest faktycznie wykonywany, nie jest w kolejności, w której go od razu odczytujesz; ponieważ ten kod mówi, co robić, ale nie określa, jak to zrobić . Zauważ, że powyższa prosta arytmetyka oczywiście nie będzie wykonywana w sekwencji zapisanej od lewej do prawej, aw powyższym przykładzie C # te 3 metody są w rzeczywistości wszystkie ponownie wprowadzane i nie są wykonywane do końca w kolejności, kompilator faktycznie generuje kod zrobi to, co chce to oświadczenie , ale niekoniecznie tak , jak zakładasz.

Uważam, że przyzwyczajenie się do tego, że kodowanie deklaratywne nie wymaga pośrednich kroków, jest naprawdę najtrudniejszą częścią tego wszystkiego; i dlaczego Haskell jest tak podstępny, ponieważ niewiele języków naprawdę tego nie dopuszcza, tak jak Haskell. Musisz zacząć robić interesującą gimnastykę, aby osiągnąć pewne rzeczy, które zwykle robiłbyś za pomocą zmiennych pośrednich, takich jak sumowanie.

W języku deklaratywnym, jeśli chcesz, aby zmienna pośrednia coś zrobiła - oznacza to przekazanie jej jako parametru do funkcji, która to robi. Właśnie dlatego rekurencja staje się tak ważna.

sum xs = (head xs) + sum (tail xs)

Nie możesz utworzyć resultSumzmiennej i dodać do niej podczas przechodzenia przez pętlę xs, musisz po prostu wziąć pierwszą wartość i dodać ją do dowolnej sumy wszystkiego innego - i uzyskać dostęp do wszystkiego innego, co musisz przekazać xsdo funkcji, tailponieważ nie możesz po prostu stworzyć zmiennej dla xs i oderwać głowę, co byłoby imperatywnym podejściem. (Tak, wiem, że możesz użyć destrukcji, ale ten przykład ma charakter poglądowy)

Jimmy Hoffa
źródło
Z twojej odpowiedzi rozumiem to, że w czystym programowaniu funkcjonalnym cały program składa się z wielu funkcji pojedynczego wyrażenia, podczas gdy w funkcjach programowania imperatywnego (procedurach) zawiera więcej niż jedno wyrażenie ze zdefiniowaną sekwencją operacji
ALXGTV
1 + 3 + (2 * 4) + 8 - (7 / (8 * 3)) to tak naprawdę wiele instrukcji / wyrażeń, oczywiście zależy to od tego, co postrzegasz jako pojedyncze wyrażenie. Podsumowując, programowanie funkcjonalne to w zasadzie programowanie bez średników.
ALXGTV
1
@ALXGTV różnica między tą deklaracją a imperatywnym stylem polega na tym, że gdyby to wyrażenie matematyczne było stwierdzeniem, konieczne byłoby , aby były one wykonywane zgodnie z opisem. Ponieważ jednak nie jest to sekwencja zdań rozkazujących, lecz wyrażenie, które definiuje to, czego chcesz, ponieważ nie mówi, jak to zrobić, nie jest konieczne, aby było wykonywane zgodnie z opisem. Dlatego kompilacja może redukować wyrażenia, a nawet zapamiętywać je jako literały. Języki imperatywne również to robią, ale tylko wtedy, gdy umieścisz je w jednym wyrażeniu; deklaracja.
Jimmy Hoffa
@ALXGTV Deklaracje definiują relacje, relacje są plastyczne; jeśli x = 1 + 2wtedy x = 3i x = 4 - 1. Instrukcje sekwencyjne definiują instrukcje, więc kiedy x = (y = 1; z = 2 + y; return z;)nie masz już czegoś plastycznego, kompilator może zrobić na wiele sposobów - raczej konieczne jest, aby kompilator zrobił to, co mu poleciłeś, ponieważ kompilator nie może znać wszystkich skutków ubocznych instrukcji, aby mógł „ zmienić je.
Jimmy Hoffa
Masz rację.
ALXGTV
6

Wiem, że spóźniłem się na przyjęcie, ale pewnego dnia miałem objawienie, więc proszę bardzo ...

Myślę, że komentarze na temat funkcjonalnego, niezmiennego i żadnych skutków ubocznych nie tracą znaczenia przy wyjaśnianiu różnicy między deklaratywnym a imperatywnym lub wyjaśnianiu, co oznacza deklaratywne programowanie. Ponadto, jak wspomniałeś w swoim pytaniu, całe „co robić” a „jak to zrobić” jest zbyt niejasne i tak naprawdę nie wyjaśnia również.

Weźmy prosty kod a = b + cjako podstawę i zobaczmy oświadczenie w kilku różnych językach, aby uzyskać pomysł:

Kiedy piszemy a = b + cw języku rozkazującym, takim jak C, przypisujemy bieżącą wartość b + czmiennej ai nic więcej. Nie składamy żadnego fundamentalnego oświadczenia na temat tego, co ajest. Raczej po prostu wykonujemy krok w procesie.

Kiedy piszemy a = b + cw języku deklaratywnym, takich jak Microsoft Excel, (tak, Excel jest językiem programowania i prawdopodobnie najbardziej deklaratywny z nich wszystkich) jesteśmy twierdząc relacji pomiędzy a, bi ctak, że zawsze jest tak, że ajest to suma Pozostałe dwa. To nie jest krok w procesie, to niezmiennik, gwarancja, deklaracja prawdy.

Języki funkcjonalne są również deklaratywne, ale prawie przypadkowo. Na przykład w Haskel a = b + crównież utrzymuje niezmienną relację, ale tylko dlatego bi cjest niezmienna.

Tak więc, gdy obiekty są niezmienne, a funkcje wolne od efektów ubocznych, kod staje się deklaratywny (nawet jeśli wygląda identycznie jak kod imperatywny), ale nie o to chodzi. Nie unika też przydziału. Celem deklaratywnego kodu jest składanie podstawowych oświadczeń o relacjach.

Daniel T.
źródło
0

Masz rację, że nie ma wyraźnego rozróżnienia między informowaniem komputera, co ma robić, a tym, jak to zrobić.

Jednak po jednej stronie spektrum prawie wyłącznie myślisz o tym, jak manipulować pamięcią. Oznacza to, że aby rozwiązać problem, prezentujesz go komputerowi w formie „ustaw tę lokalizację pamięci na x, następnie ustaw tę lokalizację pamięci na y, przeskocz do lokalizacji pamięci z ...”, a następnie jakoś wynik w innej lokalizacji pamięci.

W językach zarządzanych, takich jak Java, C # itd., Nie masz już bezpośredniego dostępu do pamięci sprzętowej. Nadrzędny programista zajmuje się teraz zmiennymi statycznymi, referencjami lub polami instancji klas, z których wszystkie są do pewnego stopnia absraktacjami dla lokalizacji pamięci.

W językach takich jak Haskell, OTOH pamięć całkowicie zniknęła. Po prostu nie jest tak w przypadku

f a b = let y = sum [a..b] in y*y

muszą być dwie komórki pamięci, które przechowują argumenty aib, a druga zawiera wyniki pośrednie y. Dla pewności backend kompilatora może emitować końcowy kod, który działa w ten sposób (iw pewnym sensie musi to robić, o ile architektura docelowa jest maszyną v. Neumann).

Ale chodzi o to, że nie musimy internalizować architektury v. Neumanna, aby zrozumieć powyższą funkcję. Nie potrzebujemy też współczesnego komputera, aby go uruchomić. Łatwo byłoby przetłumaczyć programy w czystym języku FP na hipotetyczną maszynę, która działa na przykład na podstawie rachunku SKI. Teraz spróbuj tego samego z programem C!

Dla mnie język czysto deklaratywny składałby się wyłącznie z deklaracji

To nie jest wystarczająco silne, IMHO. Nawet program C jest tylko sekwencją deklaracji. Uważam, że musimy dodatkowo doprecyzować deklaracje. Na przykład, czy mówią nam, czym coś jest (deklaratywne) lub co robi (imperatywne).

Ingo
źródło
Nie powiedziałem, że programowanie funkcjonalne nie różni się niczym od programowania w C, w rzeczywistości powiedziałem, że języki takie jak Haskell są DUŻO wyższe niż C i oferują znacznie większą abstrakcję.
ALXGTV