Czy w programowaniu funkcjonalnym lokalne zmienne zmienne bez efektów ubocznych są nadal uważane za „złą praktykę”?

23

Czy posiadanie zmiennych lokalnych zmiennych w funkcji, które są używane tylko wewnętrznie (np. Funkcja nie ma skutków ubocznych, a przynajmniej nie celowo) jest nadal uważane za „niefunkcjonalne”?

np. podczas sprawdzania stylu kursu „Programowanie funkcjonalne ze Scalą” uznaje każde varużycie za złe

Moje pytanie, jeśli funkcja nie ma skutków ubocznych, czy odradza się pisanie kodu stylu imperatywnego?

np. zamiast używać rekurencji ogona ze wzorem akumulatora, co jest złego w tworzeniu pętli local for i tworzeniu lokalnego mutable ListBufferi dodawaniu do niej, dopóki dane wejściowe nie ulegną zmianie?

Jeśli odpowiedź brzmi „tak, zawsze są zniechęcani, nawet jeśli nie ma żadnych skutków ubocznych”, to jaki jest powód?

Eran Medan
źródło
3
Wszystkie porady, nawoływania itp. Na temat, który kiedykolwiek słyszałem, odnoszą się do wspólnego stanu zmienności jako źródła złożoności. Czy ten kurs jest przeznaczony tylko dla początkujących? To prawdopodobnie celowe, nadmierne uproszczenie.
Kilian Foth,
3
@KilianFoth: Współdzielony stan mutable jest problemem w kontekstach wielowątkowych, ale nieudostępniony stan mutable może również prowadzić do trudności w uzasadnieniu programów.
Michael Shaw,
1
Myślę, że używanie lokalnej zmiennej podlegającej modyfikacji nie jest konieczną złą praktyką, ale nie jest to „styl funkcjonalny”: Myślę, że celem kursu Scala (który wybrałem podczas ostatniej jesieni) jest nauczenie programowania w stylu funkcjonalnym. Po wyraźnym rozróżnieniu stylu funkcjonalnego od imperatywnego możesz zdecydować, kiedy go użyć (na wypadek, gdyby Twój język programowania na to pozwalał). varjest zawsze niefunkcjonalny. Scala ma leniwą optymalizację zawrotów i ogonów, co pozwala całkowicie uniknąć zmiennych.
Giorgio

Odpowiedzi:

17

Jedyną rzeczą, która jest tutaj jednoznacznie złą praktyką, jest twierdzenie, że coś jest czystą funkcją, gdy tak nie jest.

Jeśli zmienne zmienne są używane w sposób, który jest naprawdę i całkowicie samowystarczalny, funkcja jest zewnętrznie czysta i wszyscy są szczęśliwi. Haskell w rzeczywistości wyraźnie to popiera , a system typów zapewnia nawet, że zmienne odwołania nie mogą być używane poza funkcją, która je tworzy.

To powiedziawszy, myślę, że mówienie o „skutkach ubocznych” nie jest najlepszym sposobem, aby na to spojrzeć (i dlatego powiedziałem „czysty” powyżej). Wszystko, co stwarza zależność między funkcją a stanem zewnętrznym, sprawia, że ​​trudniej jest o tym myśleć, a także takie rzeczy, jak znajomość aktualnego czasu lub używanie ukrytego stanu zmiennego w sposób nie bezpieczny dla wątków.

CA McCann
źródło
16

Problemem nie jest sama w sobie zmienność, to brak przejrzystości odniesienia.

Referencyjnie przejrzysta rzecz i odniesienie do niej zawsze muszą być równe, więc referencyjnie przejrzysta funkcja zawsze zwróci te same wyniki dla danego zestawu danych wejściowych, a referencyjnie przezroczysta „zmienna” jest naprawdę wartością, a nie zmienną, ponieważ nie mogę się zmienić. Możesz utworzyć referencyjnie przezroczystą funkcję, która ma wewnątrz zmienną zmienną; to nie jest problem. Jednak może być trudniej zagwarantować, że funkcja jest referencyjnie przezroczysta, w zależności od tego, co robisz.

Myślę o jednym przypadku, w którym można użyć zmienności, aby zrobić coś, co jest bardzo funkcjonalne: zapamiętywanie. Zapamiętywanie to buforowanie wartości z funkcji, więc nie trzeba ich ponownie obliczać; jest referencyjnie przezroczysty, ale wykorzystuje mutację.

Ale ogólnie przejrzystość referencyjna i niezmienność idą w parze, inne niż lokalna zmienna zmienna w referencyjnie przezroczystej funkcji i zapamiętywaniu, nie jestem pewien, czy istnieją inne przykłady, w których tak nie jest.

Michael Shaw
źródło
4
Twoja uwaga na temat zapamiętywania jest bardzo dobra. Zauważ, że Haskell mocno podkreśla przejrzystość odniesienia w programowaniu, ale podobne do zapamiętywania zachowanie leniwej oceny wymaga oszałamiającej ilości mutacji wykonywanych przez środowisko wykonawcze za sceną.
CA McCann,
@CA McCann: Myślę, że to, co mówisz, jest bardzo ważne: w funkcjonalnym języku środowisko wykonawcze może korzystać z mutacji w celu optymalizacji obliczeń, ale nie ma w języku konstrukcji umożliwiającej programistom korzystanie z mutacji. Innym przykładem jest pętla while ze zmienną pętli: w Haskell możesz napisać funkcję rekurencyjną tail, która może być zaimplementowana za pomocą zmiennej zmiennej (aby uniknąć użycia stosu), ale programista widzi niezmienne argumenty funkcji przekazywane z jednego zadzwoń do następnego.
Giorgio
@Michael Shaw: +1 za „Problemem nie jest sama w sobie zmienność, to brak przejrzystości odniesienia”. Być może możesz zacytować czysty język, w którym masz typy unikatowości: pozwalają one na zmienność, ale nadal gwarantują przejrzystość referencyjną.
Giorgio
@Giorgio: Tak naprawdę nie wiem nic o Clean, chociaż słyszałem o tym od czasu do czasu. Może powinienem to sprawdzić.
Michael Shaw,
@Michael Shaw: Niewiele wiem o Clean, ale wiem, że używa on typów unikatowych, aby zapewnić przejrzystość referencyjną. Zasadniczo można modyfikować obiekt danych, pod warunkiem że po modyfikacji nie ma żadnych odniesień do starej wartości. IMO to ilustruje twój punkt: przejrzystość referencyjna jest najważniejszym punktem, a niezmienność jest tylko jednym z możliwych sposobów zapewnienia tego.
Giorgio
8

Sprowadzanie tego do „dobrej praktyki” kontra „zła praktyka” nie jest zbyt dobre. Scala obsługuje wartości zmienne, ponieważ rozwiązują one niektóre problemy znacznie lepiej niż wartości niezmienne, a mianowicie te, które mają charakter iteracyjny.

Jeśli chodzi o perspektywę, jestem całkiem pewien, że poprzez CanBuildFromprawie wszystkie niezmienne struktury dostarczone przez scala dokonują wewnętrznej mutacji. Chodzi o to, że to, co ujawniają, jest niezmienne. Zachowanie jak największej liczby wartości niezmiennych pozwala na łatwiejsze uzasadnienie programu i zmniejszenie podatności na błędy .

Nie oznacza to, że koniecznie musisz wewnętrznie unikać struktur i wartości, które można modyfikować, gdy masz problem, który lepiej odpowiada zmienności.

Mając to na uwadze, wiele problemów, które zwykle wymagają zmiennych zmiennych (takich jak zapętlanie), można lepiej rozwiązać za pomocą wielu funkcji wyższego rzędu, które zapewniają języki takie jak Scala (mapa / filtr / fold). Bądź tego świadomy.

KChaloux
źródło
2
Tak, prawie nigdy nie potrzebuję pętli for podczas korzystania ze zbiorów Scali. map, filter, foldLeftI forEach rade przez większość czasu, ale kiedy nie, jest w stanie poczuć, że jestem „OK” w celu powrotu do brute force kod imperatyw jest miły. (o ile oczywiście nie występują żadne skutki uboczne)
Eran Medan,
3

Oprócz potencjalnych problemów z bezpieczeństwem wątków, zwykle tracisz także wiele bezpieczeństwa typu. Pętle imperatywne mają typ zwracany Uniti mogą przyjmować dowolne wyrażenie dla danych wejściowych. Funkcje wyższego rzędu, a nawet rekurencja mają znacznie bardziej precyzyjną semantykę i typy.

Masz również o wiele więcej opcji funkcjonalnego przetwarzania kontenera niż w przypadku pętli imperatywnych. Z imperatywu, zasadniczo trzeba for, whilei wariacje na temat tych dwóch drobnych jak do...whilei foreach.

W trybie funkcjonalnym masz agregację, zliczanie, filtrowanie, znajdowanie, flatMap, fold, groupBy, lastIndexWhere, map, maxBy, minBy, partycjonowanie, skanowanie, sortowanie, sortowanie, przęsło i takeWhile, aby wymienić tylko kilka bardziej popularnych z Scali standardowa biblioteka. Kiedy przyzwyczaisz się do ich dostępności, imperatywne forpętle wydają się zbyt proste w porównaniu.

Jedynym prawdziwym powodem użycia lokalnej zmienności jest bardzo rzadko wydajność.

Karl Bielefeldt
źródło
2

Powiedziałbym, że w większości jest ok. Co więcej, generowanie struktur w ten sposób może być dobrym sposobem na poprawę wydajności w niektórych przypadkach. Clojure rozwiązał ten problem, udostępniając struktury danych przejściowych .

Podstawową ideą jest dopuszczenie lokalnych mutacji w ograniczonym zakresie, a następnie zamrożenie struktury przed jej zwróceniem. W ten sposób użytkownik może nadal myśleć o kodzie, jakby był czysty, ale możesz przeprowadzać transformacje w miejscu, gdy jest to konieczne.

Jak mówi link:

Jeśli drzewo spada w lesie, czy wydaje dźwięk? Jeśli czysta funkcja mutuje niektóre dane lokalne w celu wygenerowania niezmiennej wartości zwracanej, czy to w porządku?

Simon Bergot
źródło
2

Brak lokalnych zmiennych zmiennych ma jedną zaletę - sprawia, że ​​funkcja jest bardziej przyjazna dla wątków.

Zostałem spalony przez taką zmienną lokalną (nie w moim kodzie, ani nie miałem źródła), co spowodowało uszkodzenie danych o niskim prawdopodobieństwie. Bezpieczeństwo wątków nie zostało wspomniane w ten czy inny sposób, nie było żadnego stanu, który utrzymywał się podczas połączeń i nie było żadnych skutków ubocznych. Nie przyszło mi do głowy, że to może nie być bezpieczne dla wątków, ściganie 1 na 100 000 przypadkowych uszkodzeń danych to królewski ból.

Loren Pechtel
źródło