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.
(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.Odpowiedzi:
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:
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 wszystkichi
= 0,…, n. Jest to o wiele bardziej deklaratywne niż prawie jakikolwiek program lub funkcja C.źródło
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 + y
load x; load y; add;
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
do
notacji 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.źródło
foreach
.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 ...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 ...
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ć
resultSum
zmiennej 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ćxs
do funkcji,tail
ponieważ 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)źródło
x = 1 + 2
wtedyx = 3
ix = 4 - 1
. Instrukcje sekwencyjne definiują instrukcje, więc kiedyx = (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.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 + c
jako podstawę i zobaczmy oświadczenie w kilku różnych językach, aby uzyskać pomysł:Kiedy piszemy
a = b + c
w języku rozkazującym, takim jak C, przypisujemy bieżącą wartośćb + c
zmienneja
i nic więcej. Nie składamy żadnego fundamentalnego oświadczenia na temat tego, coa
jest. Raczej po prostu wykonujemy krok w procesie.Kiedy piszemy
a = b + c
w 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ędzya
,b
ic
tak, że zawsze jest tak, żea
jest 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 + c
również utrzymuje niezmienną relację, ale tylko dlategob
ic
jest 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.
źródło
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
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!
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).
źródło