Pełne zrozumienie różnicy między proceduralnymi a funkcjonalnymi

114

Naprawdę trudno mi zrozumieć różnicę między paradygmatami programowania proceduralnego i funkcjonalnego .

Oto pierwsze dwa akapity z wpisu Wikipedii na temat programowania funkcjonalnego :

W informatyce programowanie funkcjonalne jest paradygmatem programowania, który traktuje obliczenia jako ocenę funkcji matematycznych i unika danych stanu i zmiennych. Podkreśla zastosowanie funkcji, w przeciwieństwie do imperatywnego stylu programowania, który kładzie nacisk na zmiany stanu. Programowanie funkcjonalne ma swoje korzenie w rachunku lambda, formalnym systemie opracowanym w latach trzydziestych XX wieku w celu badania definicji funkcji, zastosowania funkcji i rekurencji. Wiele funkcjonalnych języków programowania można traktować jako rozwinięcia rachunku lambda.

W praktyce różnica między funkcją matematyczną a pojęciem „funkcji” używanym w programowaniu imperatywnym polega na tym, że funkcje imperatywne mogą mieć skutki uboczne, zmieniając wartość stanu programu. Z tego powodu brakuje im referencyjnej przejrzystości, tzn. To samo wyrażenie językowe może skutkować różnymi wartościami w różnym czasie, w zależności od stanu wykonywanego programu. I odwrotnie, w kodzie funkcyjnym wartość wyjściowa funkcji zależy tylko od argumentów wprowadzonych do funkcji, więc fdwukrotne wywołanie funkcji z tą samą wartością argumentu xda ten sam wynikf(x)oba razy. Eliminacja skutków ubocznych może znacznie ułatwić zrozumienie i przewidywanie zachowania programu, co jest jedną z kluczowych motywacji dla rozwoju programowania funkcjonalnego.

W ust. 2, gdzie jest napisane

I odwrotnie, w kodzie funkcyjnym wartość wyjściowa funkcji zależy tylko od argumentów wprowadzanych do funkcji, więc fdwukrotne wywołanie funkcji z tą samą wartością argumentu xda za każdym razem ten sam wynik f(x).

Czy to nie jest dokładnie ten sam przypadek w przypadku programowania proceduralnego?

Czego należy szukać w wyróżniających się procedurach i funkcjach?

Filozof
źródło
1
Link „Uroczy Python: Programowanie funkcjonalne w Pythonie” od Abafei został uszkodzony. Oto dobry zestaw linków: ibm.com/developerworks/linux/library/l-prog/index.html ibm.com/developerworks/linux/library/l-prog2/index.html
Chris Koknat
Innym aspektem jest nazewnictwo. Na przykład. w JavaScript i Common Lisp używamy terminu funkcja, mimo że dopuszczają one efekty uboczne, aw Scheme to samo jest konsekwentnie nazywane proceduere. Czysta funkcja CL może być zapisana jako czysta procedura schematu funkcjonalnego. Prawie wszystkie książki o Scheme używają terminu procedura, ponieważ jest to typ używany w standardzie i nie ma to nic wspólnego z tym, że jest proceduralny czy funkcjonalny.
Sylwester

Odpowiedzi:

276

Programowanie funkcjonalne

Programowanie funkcjonalne odnosi się do zdolności do traktowania funkcji jako wartości.

Rozważmy analogię do „zwykłych” wartości. Możemy wziąć dwie liczby całkowite i połączyć je za pomocą +operatora, aby otrzymać nową liczbę całkowitą. Lub możemy pomnożyć liczbę całkowitą przez liczbę zmiennoprzecinkową, aby otrzymać liczbę zmiennoprzecinkową.

W programowaniu funkcjonalnym możemy połączyć dwie wartości funkcji, aby utworzyć nową wartość funkcji za pomocą operatorów takich jak compose lub lift . Lub możemy połączyć wartość funkcji i wartość danych, aby utworzyć nową wartość danych za pomocą operatorów takich jak map lub fold .

Należy zauważyć, że wiele języków ma możliwości programowania funkcjonalnego - nawet języki, które zwykle nie są uważane za języki funkcjonalne. Nawet dziadek FORTRAN obsługiwał wartości funkcji, chociaż nie oferował zbyt wiele w zakresie operatorów łączących funkcje. Aby można było nazwać język „funkcjonalnym”, musi on w dużym stopniu obejmować możliwości programowania funkcjonalnego.

Programowanie proceduralne

Programowanie proceduralne odnosi się do zdolności do hermetyzacji wspólnej sekwencji instrukcji w procedurze, tak aby te instrukcje mogły być wywoływane z wielu miejsc bez uciekania się do kopiowania i wklejania. Ponieważ procedury były bardzo wczesnym rozwojem w programowaniu, możliwości są prawie zawsze związane ze stylem programowania wymaganym przez programowanie maszynowe lub w języku asemblerowym: styl, który podkreśla pojęcie lokalizacji pamięci i instrukcji, które przenoszą dane między tymi lokalizacjami.

Kontrast

Te dwa style nie są tak naprawdę przeciwieństwami - po prostu różnią się od siebie. Istnieją języki, które w pełni obejmują oba style (na przykład LISP). Poniższy scenariusz może dać poczucie pewnych różnic w obu stylach. Napiszmy kod dla nonsensownego wymagania, w którym chcemy określić, czy wszystkie słowa na liście mają nieparzystą liczbę znaków. Po pierwsze, styl proceduralny:

function allOdd(words) {
  var result = true;
  for (var i = 0; i < length(words); ++i) {
    var len = length(words[i]);
    if (!odd(len)) {
      result = false;
      break;
    }
  }
  return result;
}

Przyjmę za pewnik, że ten przykład jest zrozumiały. Teraz funkcjonalny styl:

function allOdd(words) {
  return apply(and, map(compose(odd, length), words));
}

Działając od wewnątrz, ta definicja spełnia następujące funkcje:

  1. compose(odd, length)łączy funkcje oddi w lengthcelu utworzenia nowej funkcji, która określa, czy długość łańcucha jest nieparzysta.
  2. map(..., words)wywołuje tę nową funkcję dla każdego elementu w words, ostatecznie zwracając nową listę wartości logicznych, z których każda wskazuje, czy odpowiednie słowo ma nieparzystą liczbę znaków.
  3. apply(and, ...)stosuje operator "and" do otrzymanej listy i -ing wszystkich logicznych razem, aby uzyskać ostateczny wynik.

Na podstawie tych przykładów widać, że programowanie proceduralne jest bardzo zainteresowane przenoszeniem wartości w zmiennych i jawnym opisywaniem operacji potrzebnych do uzyskania końcowego wyniku. W przeciwieństwie do tego styl funkcjonalny kładzie nacisk na kombinację funkcji wymaganych do przekształcenia początkowego wkładu w ostateczny wynik.

Przykład pokazuje również typowe względne rozmiary kodu proceduralnego i funkcjonalnego. Ponadto pokazuje, że parametry wydajnościowe kodu proceduralnego mogą być łatwiejsze do zobaczenia niż w przypadku kodu funkcjonalnego. Zastanów się: czy funkcje obliczają długości wszystkich słów na liście, czy też każde zatrzymuje się natychmiast po znalezieniu pierwszego słowa o parzystej długości? Z drugiej strony kod funkcjonalny pozwala wysokiej jakości implementacji na wykonanie dość poważnej optymalizacji, ponieważ przede wszystkim wyraża zamiar, a nie jawny algorytm.

Dalsze czytanie

To pytanie pojawia się często ... patrz na przykład:

Wykład z nagrodą Johna Backusa Turinga szczegółowo omawia motywy programowania funkcjonalnego:

Czy programowanie można uwolnić od stylu von Neumanna?

Naprawdę nie powinienem wspominać o tym artykule w obecnym kontekście, ponieważ staje się dość techniczny, dość szybko. Po prostu nie mogłem się oprzeć, ponieważ uważam, że jest to naprawdę fundamentalne.


Dodatek - 2013

Komentatorzy zwracają uwagę, że popularne współczesne języki oferują inne style programowania, wykraczające poza proceduralne i funkcjonalne. Takie języki często oferują jeden lub więcej z następujących stylów programowania:

  • zapytanie (np. listy składane, zapytanie zintegrowane z językiem)
  • przepływ danych (np. niejawna iteracja, operacje zbiorcze)
  • zorientowane obiektowo (np. hermetyzowane dane i metody)
  • zorientowany językowo (np. składnia specyficzna dla aplikacji, makra)

Zobacz komentarze poniżej, aby zobaczyć przykłady tego, jak przykłady pseudokodów w tej odpowiedzi mogą skorzystać z niektórych udogodnień dostępnych w tych innych stylach. W szczególności przykład proceduralny odniesie korzyści z zastosowania praktycznie dowolnej konstrukcji wyższego poziomu.

Przedstawione przykłady celowo unikają mieszania się z innymi stylami programowania, aby podkreślić różnicę między dwoma omawianymi stylami.

WReach
źródło
1
Rzeczywiście fajna odpowiedź, ale czy mógłbyś trochę uprościć kod, na przykład: "function allOdd (words) {foreach (auto word in words) {odd (length (word)? Return false:;} return true;}"
Dainius
Styl funkcjonalny jest dość trudny do odczytania w porównaniu ze stylem funkcjonalnym w pythonie: def odd_words (words): return [x for x in words if odd (len (x))]
boxed
@boxed: Twoja odd_words(words)definicja robi coś innego niż odpowiedź allOdd. Do filtrowania i mapowania często preferowane są wyrażenia listowe, ale tutaj funkcja allOddma zredukować listę słów do pojedynczej wartości logicznej.
ShinNoNoir
@WReach: Twój przykład funkcjonalny napisałbym w ten sposób: function allOdd (words) {return and (odd (length (first (words))), allOdd (rest (words))); } Nie jest bardziej elegancki niż twój przykład, ale w języku rekurencyjnym ogonowym miałby takie same parametry wydajności jak styl imperatywny.
mishoo
@mishoo Język musi być zarówno rekurencyjny, jak i ścisły, a także powinien zawierać krótkie obwody w operatorze i, aby Twoje założenie się utrzymało.
kqr
46

Prawdziwą różnicą między programowaniem funkcjonalnym i imperatywnym jest sposób myślenia - programiści imperatywni myślą o zmiennych i blokach pamięci, podczas gdy programiści funkcjonalni myślą: „Jak mogę przekształcić moje dane wejściowe w moje dane wyjściowe” - Twój „program” jest potokiem i zestaw przekształceń danych, aby pobrać je z wejścia do wyjścia. To interesująca część IMO, a nie bit „Nie powinieneś używać zmiennych”.

W konsekwencji tego nastawienia programy FP zazwyczaj opisują, co się stanie, zamiast konkretnego mechanizmu tego, jak to się stanie - jest to potężne, ponieważ jeśli możemy jasno określić, co oznacza „Wybierz”, „Gdzie” i „Agreguj”, mogą swobodnie wymieniać swoje implementacje, tak jak robimy to z AsParallel () i nagle nasza aplikacja jednowątkowa skaluje się do n rdzeni.

Ana Betts
źródło
w jakikolwiek sposób można porównać te dwa fragmenty kodu przykładowego? naprawdę to doceniam
Philoxopher
1
@KerxPhilo: Oto bardzo proste zadanie (dodaj liczby od 1 do n). Tryb konieczny: zmień bieżącą liczbę, zmodyfikuj dotychczasową sumę. Kod: int i, suma; suma = 0; for (i = 1; i <= n; i ++) {sum + = i; }. Funkcjonalne (Haskell): Weź leniwą listę liczb, złóż je razem, dodając do zera. Kod: foldl (+) 0 [1..n]. Przepraszamy, brak formatowania w komentarzach.
dirkt
+1 do odpowiedzi. Innymi słowy, programowanie funkcyjne polega na pisaniu funkcji bez skutków ubocznych, gdy tylko jest to możliwe, tj. Funkcja zawsze zwraca to samo, gdy ma te same parametry - to podstawa. Jeśli zastosujesz to podejście do skrajności, twoje skutki uboczne (zawsze ich potrzebujesz) zostaną odizolowane, a pozostałe funkcje po prostu przekształcą dane wejściowe w dane wyjściowe.
beluchin
12
     Isn't that the same exact case for procedural programming?

Nie, ponieważ kod proceduralny może mieć skutki uboczne. Na przykład może przechowywać stan między połączeniami.

To powiedziawszy, możliwe jest napisanie kodu spełniającego to ograniczenie w językach uważanych za proceduralne. W niektórych językach uważanych za funkcjonalne możliwe jest również napisanie kodu, który łamie to ograniczenie.

Andy Thomas
źródło
1
Czy możesz pokazać przykład i porównanie? Naprawdę to docenisz, jeśli możesz.
Philoxopher
8
Funkcja rand () w C zapewnia inny wynik dla każdego wywołania. Przechowuje stan między połączeniami. Nie jest referencyjnie przejrzysty. Dla porównania, std :: max (a, b) w C ++ zawsze zwróci ten sam wynik przy tych samych argumentach i nie ma skutków ubocznych (o których wiem ...).
Andy Thomas,
11

Nie zgadzam się z odpowiedzią WReach. Zdekonstruujmy nieco jego odpowiedź, aby zobaczyć, skąd bierze się spór.

Najpierw jego kod:

function allOdd(words) {
  var result = true;
  for (var i = 0; i < length(words); ++i) {
    var len = length(words[i]);
    if (!odd(len)) {
      result = false;
      break;
    }
  }
  return result;
}

i

function allOdd(words) {
  return apply(and, map(compose(odd, length), words));
}

Pierwszą rzeczą, na którą należy zwrócić uwagę, jest to, że łączy:

  • Funkcjonalny
  • Zorientowany na ekspresję i
  • Iterator centryczny

programowania i brakuje możliwości programowania w stylu iteracyjnym, aby mieć bardziej wyraźny przepływ sterowania niż typowy styl funkcjonalny.

Porozmawiajmy szybko o tym.

Styl skoncentrowany na ekspresji to taki, w którym rzeczy w jak największym stopniu oceniają rzeczy. Chociaż języki funkcjonalne słyną z zamiłowania do wyrażeń, w rzeczywistości możliwe jest posiadanie języka funkcjonalnego bez wyrażeń, które można komponować. Zamierzam wymyślić jedną, w której nie ma wyrażeń, są tylko stwierdzenia.

lengths: map words length
each_odd: map lengths odd
all_odd: reduce each_odd and

Jest to prawie to samo, co podano wcześniej, z wyjątkiem tego, że funkcje są powiązane wyłącznie za pomocą łańcuchów instrukcji i powiązań.

Styl programowania zorientowany na iterator może być wykorzystany przez Pythona. Użyjmy czysto iteracyjnego stylu opartego na iteratorach:

def all_odd(words):
    lengths = (len(word) for word in words)
    each_odd = (odd(length) for length in lengths)
    return all(each_odd)

Nie jest to funkcjonalne, ponieważ każda klauzula jest procesem iteracyjnym i są one powiązane ze sobą jawną pauzą i wznowieniem ramek stosu. Składnia może być inspirowana częściowo językiem funkcjonalnym, ale jest stosowana do jego całkowicie iteracyjnego wykonania.

Oczywiście możesz to skompresować:

def all_odd(words):
    return all(odd(len(word)) for word in words)

Imperatyw nie wygląda teraz tak źle, co? :)

Ostatni punkt dotyczył bardziej wyraźnego przepływu kontroli. Przepiszmy oryginalny kod, aby wykorzystać to:

function allOdd(words) {
    for (var i = 0; i < length(words); ++i) {
        if (!odd(length(words[i]))) {
            return false;
        }
    }
    return true;
}

Używając iteratorów, możesz mieć:

function allOdd(words) {
    for (word : words) { if (!odd(length(word))) { return false; } }
    return true;
}

Jaki jest więc sens języka funkcjonalnego, jeśli różnica jest między:

return all(odd(len(word)) for word in words)
return apply(and, map(compose(odd, length), words))
for (word : words) { if (!odd(length(word))) { return false; } }
return true;


Główną ostateczną cechą funkcjonalnego języka programowania jest to, że usuwa on mutacje jako część typowego modelu programowania. Ludzie często rozumieją, że oznacza to, że funkcjonalny język programowania nie ma instrukcji lub używa wyrażeń, ale są to uproszczenia. Język funkcyjny zastępuje jawne obliczenia deklaracją zachowania, na podstawie której język dokonuje redukcji.

Ograniczenie się do tego podzbioru funkcji pozwala mieć więcej gwarancji dotyczących zachowania programów, a to pozwala na swobodne ich komponowanie.

Jeśli masz język funkcjonalny, tworzenie nowych funkcji jest zwykle tak proste, jak tworzenie blisko powiązanych funkcji.

all = partial(apply, and)

Nie jest to proste, a może nawet niemożliwe, jeśli nie masz jawnie kontrolowanych globalnych zależności funkcji. Najlepszą cechą programowania funkcjonalnego jest to, że można konsekwentnie tworzyć bardziej ogólne abstrakcje i mieć pewność, że można je połączyć w większą całość.

Veedrac
źródło
Wiesz, jestem prawie pewien, applyże operacja nie jest tym samym co operacja foldlub reduce, chociaż zgadzam się z niezłą możliwością posiadania bardzo ogólnych algorytmów.
Benedict Lee,
Nigdy nie słyszałem o applyznaczeniu foldlub reduce, ale wydaje mi się, że w tym kontekście musi to być, aby zwracało wartość logiczną.
Veedrac,
Ach, ok, pomyliłem się przez nazewnictwo. Dzięki za wyjaśnienie tego.
Benedict Lee
6

W paradygmacie proceduralnym (czy zamiast tego powinienem powiedzieć „programowanie strukturalne”?), Masz współdzieloną zmienną pamięć i instrukcje, które odczytują / zapisują ją w określonej kolejności (jedna po drugiej).

W paradygmacie funkcjonalnym masz zmienne i funkcje (w sensie matematycznym: zmienne nie zmieniają się w czasie, funkcje mogą tylko obliczać coś na podstawie swoich danych wejściowych).

(Jest to nadmiernie uproszczone, np. FPL zazwyczaj mają ułatwienia do pracy z pamięcią zmienną, podczas gdy języki proceduralne mogą często obsługiwać procedury wyższego rzędu, więc rzeczy nie są tak jasne; ale to powinno dać ci pomysł)

Artem Shalkhakov
źródło
2

Charming Python: Programowanie funkcyjne w Pythonie z IBM Developerworks naprawdę pomógł mi zrozumieć różnicę.

Zwłaszcza dla kogoś, kto trochę zna Pythona, przykłady kodu w tym artykule, w których robienie różnych rzeczy funkcjonalnie i proceduralnie są skontrastowane, mogą wyjaśnić różnicę między programowaniem proceduralnym i funkcjonalnym.

Abbafei
źródło
2

W programowaniu funkcjonalnym, aby zrozumieć znaczenie symbolu (nazwa zmiennej lub funkcji), tak naprawdę wystarczy znać 2 rzeczy - aktualny zakres i nazwę symbolu. Jeśli masz czysto funkcjonalny język z niezmiennością, oba te pojęcia są „statyczne” (przepraszam za bardzo przeciążoną nazwę), co oznacza, że ​​możesz zobaczyć zarówno bieżący zakres, jak i nazwę - po prostu patrząc na kod źródłowy.

W programowaniu proceduralnym, jeśli chcesz odpowiedzieć na pytanie, jaka jest wartość za xtobą, musisz również wiedzieć, jak się tam dostałeś, sam zakres i nazwa nie wystarczą. I właśnie to uznałbym za największe wyzwanie, ponieważ ta ścieżka wykonania jest właściwością „runtime” i może zależeć od tak wielu różnych rzeczy, że większość ludzi uczy się po prostu debugować, a nie próbować odzyskać ścieżkę wykonania.

wasily
źródło
1

Niedawno zastanawiałem się nad różnicą związaną z problemem wyrażenia . Opis Phila Wadlera jest często cytowany, ale zaakceptowana odpowiedź na to pytanie jest prawdopodobnie łatwiejsza do zrozumienia. Zasadniczo wydaje się, że języki imperatywne wybierają jedno podejście do problemu, podczas gdy języki funkcjonalne wybierają drugie.

John L.
źródło
0

Jedną wyraźną różnicą między tymi dwoma paradygmatami programowania jest stan.

W programowaniu funkcyjnym stan jest unika. Mówiąc najprościej, nie będzie zmiennej, której zostanie przypisana wartość.

Przykład:

def double(x):
    return x * 2

def doubleLst(lst):
    return list(map(double, action))

Jednak programowanie proceduralne używa stanu.

Przykład:

def doubleLst(lst):
    for i in range(len(lst)):
        lst[i] = lst[i] * 2  # assigning of value i.e. mutation of state
    return lst
Tox
źródło