Czy w programowaniu funkcjonalnym zamknięcia są uważane za nieczyste?
Wydaje się, że ogólnie można uniknąć zamknięć, przekazując wartości bezpośrednio do funkcji. Dlatego należy w miarę możliwości unikać zamykania?
Jeśli są one nieczyste i słusznie stwierdzam, że można ich uniknąć, dlaczego tak wiele funkcjonalnych języków programowania obsługuje zamykanie?
Jednym z kryteriów czystej funkcji jest to, że „Funkcja zawsze ocenia tę samą wartość wyniku, biorąc pod uwagę te same wartości argumentu ”.
Przypuszczać
f: x -> x + y
f(3)
nie zawsze da taki sam rezultat. f(3)
zależy od wartości, y
której nie jest argumentem f
. Zatem f
nie jest to czysta funkcja.
Ponieważ wszystkie zamknięcia opierają się na wartościach, które nie są argumentami, jak to możliwe, aby każde zamknięcie było czyste? Tak, teoretycznie wartość zamknięta może być stała, ale nie ma możliwości, aby się tego dowiedzieć, po prostu patrząc na kod źródłowy samej funkcji.
To prowadzi mnie do tego, że ta sama funkcja może być czysta w jednej sytuacji, ale nieczysta w innej. Nie zawsze można ustalić, czy funkcja jest czysta, czy nie, studiując jej kod źródłowy. Być może trzeba będzie rozważyć to w kontekście otoczenia w momencie, w którym jest wywoływane, zanim będzie można dokonać takiego rozróżnienia.
Czy myślę o tym poprawnie?
źródło
y
nie można go zmienić, więc wynikf(3)
zawsze będzie taki sam.y
jest częścią definicji,f
mimo że nie jest wyraźnie oznaczony jako dane wejściowef
- nadal jest to przypadekf
zdefiniowany w kategoriachy
(moglibyśmy oznaczyć funkcję f_y, aby uzależnić ją ody
jawnej), a zatem zmianay
daje inną funkcję . Określona funkcjaf_y
określona dla określonegoy
jest bardzo czysta. (Na przykład dwie funkcjef: x -> x + 3
if: x -> x + 5
są różnymi funkcjami, i obie są czyste, mimo że zdarzyło nam się użyć tej samej litery, aby je oznaczyć.)Odpowiedzi:
Czystość można zmierzyć za pomocą dwóch rzeczy:
Jeśli odpowiedź na 1 brzmi „tak”, a odpowiedź na „2” nie, to funkcja jest czysta. Zamknięcia powodują, że funkcja staje się nieczysta tylko wtedy, gdy zmodyfikujesz zmienną zamkniętą.
źródło
y
. Na przykład definiujemy funkcjęg
taką, którag(y)
sama jest funkcjąx -> x + y
. Następnieg
jest funkcją liczb całkowitych, która zwraca funkcje,g(3)
jest funkcją liczb całkowitych, która zwraca liczby całkowite, ig(2)
jest inną funkcją liczb całkowitych, która zwraca liczby całkowite. Wszystkie trzy funkcje są czyste.Zamknięcia pojawiają się Lambda Calculus, który jest najczystszą możliwą formą programowania funkcjonalnego, więc nie nazwałbym ich „nieczystymi” ...
Zamknięcia nie są „nieczyste”, ponieważ funkcje w językach funkcjonalnych są pierwszorzędnymi obywatelami - oznacza to, że można je traktować jako wartości.
Wyobraź to sobie (pseudokod):
y
jest wartością. Jego wartość zależy odx
, alex
jest niezmienna, więcy
wartość jest również niezmienna. Możemy wywoływaćfoo
wiele razy z różnymi argumentami, które wytworzą różney
s, aley
wszystkie one żyją w różnych zakresach i zależą od różnychx
s, więc czystość pozostaje nienaruszona.Teraz zmieńmy to:
Tutaj używamy zamknięcia (zamykamy x), ale jest to to samo co w
foo
- różne wywołaniabar
z różnymi argumentami tworzą różne wartościy
(pamiętaj - funkcje są wartościami), które są niezmienne, więc czystość pozostaje nienaruszona.Należy również pamiętać, że zamknięcia mają bardzo podobny efekt do curry:
baz
tak naprawdę nie różni się odbar
- w obu tworzymy funkcję o nazwie,y
która zwraca jej argument plusx
. W rzeczywistości w Lambda Calculus używasz zamknięć do tworzenia funkcji z wieloma argumentami - i nadal nie jest to nieczyste.źródło
Inni ładnie opisali ogólne pytanie w swoich odpowiedziach, więc przyjrzę się tylko usunięciu zamieszania, które zasygnalizowałeś w swojej edycji.
Zamknięcie nie staje się wejściem funkcji, a raczej „wchodzi” do treści funkcji. Mówiąc ściślej, funkcja odnosi się do wartości w zewnętrznym zakresie w jej ciele.
Masz wrażenie, że funkcja ta jest nieczysta. Zasadniczo tak nie jest. W programowaniu funkcjonalnym wartości są niezmienne przez większość czasu . Dotyczy to również wartości zamkniętej.
Powiedzmy, że masz taki kod:
Wywoływanie
make 3
imake 4
daje dwie funkcje z zamknięciami ponadmake
„sy
argument. Jeden wrócix + 3
, drugix + 4
. Są to jednak dwie odrębne funkcje i obie są czyste. Zostały one utworzone przy użyciu tej samejmake
funkcji, ale to wszystko.Zauważ najczęściej kilka akapitów wstecz.
Pamiętaj, że dla 2 i 3 języki te nie dają żadnych gwarancji czystości. Zanieczyszczenia nie są właściwością zamknięcia, ale samym językiem. Zamknięcia same w sobie nie zmieniają obrazu.
źródło
IO A
wartość, a twój typ zamknięcia jestIO (B -> C)
lub taki. Czystość została utrzymanaZwykle prosiłbym cię o wyjaśnienie definicji „nieczystej”, ale w tym przypadku tak naprawdę nie ma to znaczenia. Zakładając, że kontrastujesz go z terminem czysto funkcjonalnym , odpowiedź brzmi „nie”, ponieważ nie ma nic o zamknięciach, które z natury są destrukcyjne. Jeśli twój język byłby czysto funkcjonalny bez zamknięć, nadal byłby czysto funkcjonalny z zamknięciami. Jeśli zamiast tego masz na myśli „niefunkcjonalny”, odpowiedź brzmi „nie”; zamknięcia ułatwiają tworzenie funkcji.
Tak, ale wtedy twoja funkcja miałaby jeszcze jeden parametr, a to zmieniłoby jej typ. Zamknięcia umożliwiają tworzenie funkcji opartych na zmiennych bez dodawania parametrów. Jest to przydatne, gdy masz, powiedzmy, funkcję, która przyjmuje 2 argumenty i chcesz utworzyć jej wersję, która przyjmuje tylko 1 argument.
EDYCJA: W odniesieniu do twojej edycji / przykładu ...
Zależy to od złego wyboru słowa tutaj. Cytując ten sam artykuł w Wikipedii, który napisałeś:
Zakładając, że
y
jest niezmienny (co zwykle ma miejsce w językach funkcjonalnych), warunek 1 jest spełniony: dla wszystkich wartościx
wartośćf(x)
nie zmienia się. Powinno to wynikać z faktu, żey
niczym nie różni się od stałej ix + 3
jest czyste. Jest również jasne, że nie ma mutacji ani operacji we / wy.źródło
Bardzo szybko: podstawienie jest „referencyjnie przezroczyste”, jeśli „podstawienie podobnego prowadzi do podobnego”, a funkcja jest „czysta”, jeśli wszystkie jej efekty są zawarte w jej wartości zwracanej. Oba można sprecyzować, ale należy pamiętać, że nie są one identyczne, ani nawet nie implikuje drugiego.
Porozmawiajmy teraz o zamknięciach.
Nudne (głównie czyste) „zamknięcia”
Zamknięcia występują, ponieważ podczas oceny terminu lambda interpretujemy (powiązane) zmienne jako wyszukiwania środowiska. Zatem, gdy zwrócimy termin lambda w wyniku oceny, zmienne w nim zawarte „zamkną” wartości, które przyjęli, gdy zostały zdefiniowane.
W prostym rachunku lambda jest to trochę banalne i całe pojęcie po prostu znika. Aby to wykazać, oto stosunkowo lekki interpreter rachunku lambda:
Ważną kwestią, na którą należy zwrócić uwagę, jest fakt,
addEnv
że rozszerzamy środowisko o nową nazwę. Ta funkcja jest nazywana tylko „wewnątrz” interpretowanegoAbs
terminu trakcji (lambda). Środowisko jest „sprawdzane” za każdym razem, gdy oceniamy danyVar
termin, a zatemVar
rozwiązują one wszystko,Name
o czym mowa w tym,Env
co zostało przechwycone przezAbs
przyczepę zawierającąVar
.Teraz znowu, w kategoriach LC, jest to nudne. Oznacza to, że zmienne powiązane są tylko stałymi, o ile kogokolwiek to obchodzi. Są one oceniane bezpośrednio i natychmiast, jako wartości, które oznaczają w środowisku, jak do tego momentu, w zakresie leksykalnym.
Jest to również (prawie) czyste. Jedyne znaczenie dowolnego terminu w naszym rachunku lambda zależy od jego wartości zwracanej. Jedynym wyjątkiem jest efekt uboczny braku rozwiązania, który jest ujęty w nazwie Omega:
Interesujące (nieczyste) zamknięcia
Teraz, dla niektórych środowisk, zamknięcia opisane w zwykłym LC powyżej są nudne, ponieważ nie ma pojęcia, że będziemy w stanie oddziaływać na zmienne, które zamknęliśmy. W szczególności słowo „zamknięcie” ma tendencję do wywoływania kodu, takiego jak poniższy Javascript
To pokazuje, że zamknęliśmy
n
zmienną w funkcji wewnętrznejincr
i wywoływanie w sposóbincr
znaczący współdziała z tą zmienną.mk_counter
jest czysty, aleincr
zdecydowanie nieczysty (i nie jest również względnie przejrzysty).Co różni się między tymi dwoma instancjami?
Pojęcia „zmiennej”
Jeśli spojrzymy na znaczenie substytucji i abstrakcji w prostym sensie LC, zauważymy, że są one zdecydowanie jasne. Zmienne są dosłownie niczym więcej niż bezpośrednimi przeglądami środowiska. Abstrakcja Lambda jest dosłownie niczym więcej niż stworzeniem rozszerzonego środowiska do oceny wewnętrznego wyrażenia. W tym modelu nie ma miejsca na zachowanie, które widzieliśmy z
mk_counter
/,incr
ponieważ nie jest dozwolona żadna odmiana.Dla wielu jest to sedno tego, co oznacza „zmienna” - zmienność. Jednak semantycy lubią rozróżniać rodzaj zmiennej używanej w LC od rodzaju „zmiennej” używanej w Javascript. Aby to zrobić, nazywają to „komórką zmienną” lub „gniazdem”.
Ta nomenklatura nawiązuje do długiego historycznego użycia „zmiennej” w matematyce, gdzie oznaczała coś bardziej jak „nieznany”: (matematyczne) wyrażenie „x + x” nie pozwala
x
zmieniać się w czasie, zamiast tego ma znaczenie niezależnie wartości (pojedynczej, stałej)x
trwa.Dlatego mówimy „szczelina”, aby podkreślić możliwość umieszczania wartości w szczelinie i wyjmowania ich.
Aby dodać zamieszanie, w Javascript, te „sloty” wyglądają tak samo jak zmienne: piszemy
stworzyć taki, a potem, kiedy piszemy
wskazuje, że szukamy wartości aktualnie przechowywanej w tym gnieździe. Aby to wyjaśnić, czyste języki mają tendencję do myślenia o gniazdach jako nazwach (rachunku matematycznym, rachunku lambda). W takim przypadku musimy wyraźnie oznaczyć, kiedy otrzymujemy lub wkładamy z automatu. Taka notacja zwykle wygląda
Zaletą tego zapisu jest to, że mamy teraz wyraźne rozróżnienie między zmiennymi matematycznymi a zmiennymi szczelinami. Zmienne mogą przyjmować boksy jako ich wartości, ale konkretny boks nazwany przez zmienną jest stały w całym jego zakresie.
Za pomocą tej notacji możemy przepisać
mk_counter
przykład (tym razem w składni podobnej do Haskella, choć zdecydowanie nie podobnej do semantyki podobnej do Haskella):W tym przypadku używamy procedur, które manipulują tym zmiennym gniazdem. Aby go zaimplementować, musielibyśmy zamknąć nie tylko stałe środowisko nazw,
x
ale także zmienne środowisko zawierające wszystkie potrzebne gniazda. Jest to bliższe powszechnemu pojęciu „zamknięcia”, które ludzie tak bardzo kochają.Znowu
mkCounter
jest bardzo nieczyste. Jest także bardzo referencyjnie nieprzejrzysty. Zauważ jednak, że skutki uboczne nie wynikają z przechwytywania lub zamykania nazwy, ale z przechwytywania modyfikowalnej komórki i działań niepożądanych na niej, takich jakget
iput
.Ostatecznie myślę, że jest to ostateczna odpowiedź na twoje pytanie: na czystość nie wpływa (matematyczne) przechwytywanie zmiennych, ale zamiast tego działania niepożądane wykonywane na zmiennych gniazdach nazwanych przez przechwycone zmienne.
Tyle tylko, że w językach, które nie próbują być blisko LC ani nie próbują zachować czystości, te dwa pojęcia są tak często splecione, co prowadzi do zamieszania.
źródło
Nie, zamknięcia nie powodują nieczystości funkcji, o ile wartość zamknięcia jest stała (nie zmieniana przez zamknięcie ani inny kod), co jest typowym przypadkiem w programowaniu funkcjonalnym.
Zauważ, że chociaż zawsze możesz zamiast tego podać wartość jako argument, zwykle nie możesz tego zrobić bez znacznych trudności. Na przykład (coffeescript):
Według Twojej sugestii możesz po prostu wrócić:
Ta funkcja nie jest w tym momencie wywoływana , tylko zdefiniowana , więc musisz znaleźć sposób na przekazanie pożądanej wartości
closedValue
do punktu, w którym funkcja jest faktycznie wywoływana. W najlepszym wypadku tworzy to wiele sprzężeń. W najgorszym przypadku nie kontrolujesz kodu w punkcie wywoływania, więc jest to praktycznie niemożliwe.Biblioteki zdarzeń w językach, które nie obsługują zamknięć, zwykle zapewniają inny sposób przekazywania dowolnych danych z powrotem do wywołania zwrotnego, ale nie są ładne i powodują wiele trudności zarówno dla osoby zarządzającej biblioteką, jak i dla użytkowników biblioteki.
źródło