Closure

0

Proste rekurencyjne wypisanie kolejnych liczb.

Jestem uczony w stylu starej daty, napisałbym bez wahania w stylu Old.

function logNumbersRecursivelyOld(start, end) {
    console.log(`Recursively looping from ${start} until ${end}`);

    function recurse(left, right) {
        console.log(`Hitting index ${left}`);

        if (left < right) {
            recurse(left + 1, right);
        }
    }

    recurse(start, end);
}

Niedawno usłyszałem, że tak nadmiarowo to się już nie pisze, tylko tak:

function logNumbersRecursivelyNew(start, end) {
    console.log(`Recursively looping from ${start} until ${end}`);

    function recurse(left) {
        console.log(`Hitting index ${left}`);

        if (left < end) {
            recurse(left + 1);
        }
    }

    recurse(start);
}

Wywołanie jawne recurse(left, right) pisze się teraz recurse(left) a aktualna wartość argumentu right idzie w closure?
Jak dla mnie stary (przestarzały?) styl z jawnym przekazaniem obu argumentów function recurse(left, right) jest bardziej czytelny.

0

Jaki jest problem? Jeżeli w języku jest domknięcie "closure", to obydwa listingi są równoważne (czasowo i pamięciowo).

0

Niby w starym stylu w takich językach piszą tylko dinozaury.

0

A to już inna sprawa, jak Jesteś "cool kid", to wiadomo jak Napiszesz;)

5

Od jakiegoś czasu czytam i próbuję zrozumieć "Struktura i interpretacja programów komputerowych" z 1996 (Second Edition) i mam wrażenie że ten New Style to tak naprawdę Old Old Style. po prostu wiele języków nie posiadało dobrze działających closure więc trzeba było kantować. W Scheme zawsze były closure więc można było je używać.

0

Na szybko bo zostałem przez górę pogoniony do** pilnej roboty jeszcze na dziś**
Mamy function as first class citizen to musimy mieć closures żeby wszystko działało modelowo

let tank = 'Rudy';

function battle() {
    console.log(tank + 'destroyed Tiger');
}

setTimeout(battle, 1000);

Specjalnie setTimeout bo to już przekazanie funkcji do innej "biblioteki" w której to scope nie ma widocznej zmiennej tank.

Bez closure, z parametrem byłoby kombinowanie

tank = {
    name: 'T-34'
};

function kursk(name) {
    console.log(this.name + ' fought & won');
}

setTimeout(kursk.bind(tank), 1000);

Tylko IMHO wpakowywanie closure wszędzie - jak np do rekurencji - prostych funkcji pomocniczych - bo closure jest cool cool czyni kod dużo mniej czytelnym

1
BraVolt napisał(a):

Tylko IMHO wpakowywanie closure wszędzie - jak np do rekurencji - prostych funkcji pomocniczych - bo closure jest cool cool czyni kod dużo mniej czytelnym

No i niestety tu wszystko kończy się na "IHMO". Np kiedyś czytałem że prywatne metody zawsze powinny być statyczne, żeby było widać które metody korzystają z których pól w obiekcie. (Zalecenie dla języka Java)

W obu wypadkach można się kłócić czy lepiej przekazywać zmienną jawnie przez parametr, czy lepiej żeby była to zmienna wolna. IHMO jeśli klasa/funkcja lub ogólnie zakres leksykalny jest mały i mieści się na jednym ekranie to parametr tylko zaciemnia.

0

"Tylko IMHO wpakowywanie closure wszędzie - jak np do rekurencji - prostych funkcji pomocniczych - bo closure jest cool cool czyni kod dużo mniej czytelnym" - tam gdzie pasuje zastosowanie closure kod na pewno będzie czytelniejszy, Python, podaję za "Fluent Python":

def make_average():
	s = []
	def ave(n):
		s.append(n)
		return sum(s) / len(s)
	return ave

>>> a = make_average()
>>> a(1)
1.0
>>> a(2)
1.5
>>> a(3)
2.0
>>> a(4)
2.5

1

"Nie znam się na średniej kroczącej, może jej wykorzystanie narzuca takie działanie; jednak jeśli nawet, to przechowywanie tablicy w funkcji wydaje mi się nieintuicyjne w tym przypadku"
To jest właśnie wykorzystanie domknięcia, i HOF's, można to samo osiągnąć tworząc obiekt, ale, jak dla mnie opcja funkcyjna jest czytelniejsza i ładniejsza.

class Averager():
    def __init__(self):
        self.series = []
    def __call__(self, new_value):
        self.series.append(new_value)
        total = sum(self.series)
        return total/len(self.series)
2

Jak dla mnie stary (przestarzały?) styl z jawnym przekazaniem obu argumentów function recurse(left, right) jest bardziej czytelny.

Może czytelny, ale bez domknięcia męczysz stos przekazywaniem w kółko parametru right, który jest stały w każdym wywołaniu. Więc tu raczej nie moda gra rolę, ale względy praktyczne. Rekurencyjne metody do czytelnych nie należą, więc domknięcie tu raczej dużo nie popsuje.

W Javie też da się zrobić takie "bieda" domknięcie, tylko kod nie wygląda zbyt elegancko:

class Recursive<I>{
    public I func;
}
...
void logNumbersRecursivelyNew(int start, int end) {
        Recursive<Function<Integer, Void>> recurse = new Recursive<>();
        recurse.func  = left -> {
            System.out.printf("Hitting index %d\n", left);
            if (left < end){
                recurse.func.apply(left+1);
            }
            return null;
        };
        recurse.func.apply(start);
    }
1

Może czytelny, ale bez domknięcia męczysz stos przekazywaniem w kółko parametru right, który jest stały w każdym wywołaniu.

Tylko jeśli nie ma TCO, co w większości języków już jest. A nawet jak nie ma TCO, to jeśli kompilator/interpreter optymalizuje, to to zostanie wyoptymalizowane, zwłaszcza jeśli IR używa SSA.

3

@Silv

Czy rozróżnienie między obiektem a funkcją jest istotne według Ciebie w tym przypadku (Python+średnia)? Według mnie tak, abstrahując od Pythona. Przy obliczaniu średniej obiekt ostatecznie może dla mnie przechowywać tablicę – aczkolwiek mało intuicyjny, również, jest dla mnie obiekt o nazwie Averager. Natomiast funkcja nie powinna jej przechowywać (jako domknięcie).

  The venerable master Qc Na was walking with his student, Anton.  Hoping to
prompt the master into a discussion, Anton said "Master, I have heard that
objects are a very good thing - is this true?"  Qc Na looked pityingly at
his student and replied, "Foolish pupil - objects are merely a poor man's
closures."

  Chastised, Anton took his leave from his master and returned to his cell,
intent on studying closures.  He carefully read the entire "Lambda: The
Ultimate..." series of papers and its cousins, and implemented a small
Scheme interpreter with a closure-based object system.  He learned much, and
looked forward to informing his master of his progress.

  On his next walk with Qc Na, Anton attempted to impress his master by
saying "Master, I have diligently studied the matter, and now understand
that objects are truly a poor man's closures."  Qc Na responded by hitting
Anton with his stick, saying "When will you learn? Closures are a poor man's
object."  At that moment, Anton became enlightened.

:D

Źródło: http://people.csail.mit.edu/gregs/ll1-discuss-archive-html/msg03277.html

0
hauleth napisał(a):

Może czytelny, ale bez domknięcia męczysz stos przekazywaniem w kółko parametru right, który jest stały w każdym wywołaniu.

Tylko jeśli nie ma TCO, co w większości języków już jest. A nawet jak nie ma TCO, to jeśli kompilator/interpreter optymalizuje, to to zostanie wyoptymalizowane, zwłaszcza jeśli IR używa SSA.

Nie znam za dobrze JS, ale chyba tylko Safari ma TCO.
Prosty test po ilu rekurencjach następuje przepełnienie stosu pokazuje, że interpreter JS nie robi żadnej optymalizacji:

var size = 100;
while(true){
  try {
        logNumbersRecursivelyOld(1, size);
        size += 100;
  } catch (e) {
        console.log("Old - stack overflow after " +size + " recursions");
        break;
    }
}
size = 100;
while(true){
    try {
        logNumbersRecursivelyNew(1, size);
        size += 100;
    } catch (e) {
        console.log("New - stack overflow after " +size + " recursions");
        break;
    }
}

Wynik

Old - stack overflow after 14000 recursions
New - stack overflow after 15700 recursions
3

@Silv:

Czy rozróżnienie między obiektem a funkcją jest istotne według Ciebie w tym przypadku (Python+średnia)? Według mnie tak, abstrahując od Pythona. Przy obliczaniu średniej obiekt ostatecznie może dla mnie przechowywać tablicę – aczkolwiek mało intuicyjny, również, jest dla mnie obiekt o nazwie Averager. Natomiast funkcja nie powinna jej przechowywać (jako domknięcie).

Tak jak napisał @Maciej Cąderek nie ma większej róźnicy między obiektem a closure. Zauważ jednak, że używając closure, tablica nie może być widoczna z zewnątrz, co ogranicza potencjalne błędy.

Co do kwestii czy ta tablica w ogóle powinna być przechowywana w funkcji, to moim zdaniem w tym przypadku odpowiedź brzmi tak. Wyobraź sobie sytuację w której masz jakąś pętle obliczeń i w każdym kroku musisz policzyć 5 różnych średnich kroczących dla różnych danych. Jeśli wyrzucisz "stan" z funkcji liczącej MA to będziesz go musiał obsłużyć w tej pętli i zaśmiecisz sobie cały kod update'owaniem tablic, a jak będziesz chciał to zrobić wydajniej to musisz trzymać sumę i liczbę elementów, więc na każde obliczenie średniej potrzebujesz 3 linijek kodu zamiast 1, co gorsza w miejscu, gdzie powinieneś mieć logikę biznesową (powiedzmy jakąś formułę matematyczną do obliczania metryki) masz szczegóły implementacji MA. Ten pattern jest na tyle częsty, że nawet w niemutowalnym języku jakim jest Haskell możesz pisać w ten sposób używając StateMonad

Co do pierwotnego pytania, to ja preferuję wersję z closure. Uważam, że używanie redundantnego parametru, dodaje funkcji wewnętrznej niepotrzebnej generyczności i zaciemnia cel, dla którego została napisana.

0

Jestem zwolennikiem prostoty i czytelności kodu, co za tym idzie unikania specyficznych rozwiązań. Może to kwestia tego, że czytam kod sprzed 5, 10 lat i mam świadomość, że za następne 5 lat ten kod, poprawiany i modyfikowany, nadal będzie żył.

0
Kamil Żabiński napisał(a):

Od jakiegoś czasu czytam i próbuję zrozumieć "Struktura i interpretacja programów komputerowych" z 1996 (Second Edition) i mam wrażenie że ten New Style to tak naprawdę Old Old Style. po prostu wiele języków nie posiadało dobrze działających closure więc trzeba było kantować. W Scheme zawsze były closure więc można było je używać.

I jeszcze odnośnie braku implementacji clusure w Java.

Albo closure albo pure function.
Paradygmat funkcyjny został niejako odkryty na nowo (albo odkurzony) bo fajnie wpasowuje się w programowanie rozproszone, skalowalność itp.

Po wyeliminowaniu closures (i side effects) tak przykrojony język dobrze pasuje do obecnych wymagań.

0

"Albo closure albo pure function" - Mógłbyś to rozwinąć, co Masz na myśli?

1

https://medium.com/javascript-scene/master-the-javascript-interview-what-is-a-pure-function-d1c076bec976
https://codeburst.io/shared-mutability-handle-with-care-351b8fa2f939

Góogle: Closure vs Pure Functions i dyskusje

Java jest mocno współbieżna, tak bym to określił, Pure JS raczej single threaded. Oracle świadomie podjęło decyzję o nieimplementowaniiu closures, w JS w niczym one nie przeszkadzają a przy tym czynią z JS język 'prawdziwie funkcyjny' (święta wojna czy Java jest językiem funkcyjnym czy nie i dlaczego JS pokazuje wyższość nad Javą. Zażartość dyskusji może miejscami tylko niewiele odbiegać od dyskusji Android vs. iOS).

Moje zdanie jest takie, że język - narzędzie dobieramy do zadania, samym narzędziem posługujemy się bez hardcoru, żeby w przyszłości w miarę prosto i bezpiecznie można było zmienić narzędzie bez przewracania kodu do góry nogami w celu wyeliminowania kruczków konkretnego języka - narzędzia.

Na jednej z prelekcji na temat programowania funkcyjnego. jak pamiętam w Krakowie, Venkat Subramaniam ładnie ujął temat, mniej więcej tak
sharing is good
mutability is good
sharing mutability is evil

0

W Javie od wersji 8 masz closures, wcześniej ich nie było, bo nie było funkcji anonimowych, zresztą jak już było napisane closure i obiekty to właściwie to samo, więc można powiedzieć że Java od zawsze miała closures.

Pure functions i closure to są różne rzeczy, chyba każdy język funkcyjny ma domknięcia, to jest semantyka przeniesiona wprost z rachunku lambda. W Haskellu bez domknięć ciężko coś napisać, a to przecież czysty język (poza IO).

0

@BraVolt: Po pierwsze, źle to odczytałem - widziałem tam closures vs functions as first class citizens:). Ale do rzeczy, ano nie, nie ma dyskusji, powyżej jest o rachunku lambda. Jeżeli jesteś w języku funkcyjnym, to domknięcie jest nad wartością immutable, więc czystość funkcji nie jest złamana; w przeciwieństwie do powyższego przykładu z Pythona, gdzie domykamy nad mutable listą i jeszcze zmieniamy ją w funkcji.

0

Z tą czystością jest jak z tail recursion w JVMkach. Niby miała być, niby są języki na JVM gdzie to hula znakomicie, a w większości przypadków nie ma.
W JS pure function nie musimy się za bardzo przejmować, co innego w Java.

1 użytkowników online, w tym zalogowanych: 0, gości: 1