Czy funkcje generatora są prawidłowe w programowaniu funkcjonalnym?

17

Pytania są następujące:

  • Czy generatory łamią paradygmat programowania funkcjonalnego? Dlaczego lub dlaczego nie?
  • Jeśli tak, czy generatory mogą być używane w programowaniu funkcjonalnym i jak?

Rozważ następujące:

function * downCounter(maxValue) {
  yield maxValue;
  yield * downCounter(maxValue > 0 ? maxValue - 1 : 0);
}

let counter = downCounter(26);
counter.next().value; // 26
counter.next().value; // 25
// ...etc

downCounterMetodą wydaje bezstanowy. Również wywoływanie downCounterz tym samym wejściem zawsze będzie skutkować tym samym wyjściem. Jednocześnie jednak wywoływanie next()nie daje spójnych wyników.

Nie jestem pewien, czy generatory łamią paradygmat programowania funkcjonalnego, ponieważ w tym przykładzie counterjest obiektem generatora, a więc wywołanie next()dałoby takie same wyniki jak inny obiekt generatora utworzony z dokładnie takim samym maxValue.

Również wywołanie someCollection[3]tablicy zawsze zwróci czwarty element. Podobnie, next()czterokrotne wywołanie obiektu generatora zawsze zwróci czwarty element.

Dla większego kontekstu pytania te zostały podniesione podczas pracy nad kata programistycznym . Osoba, która odpowiedziała na pytanie, zadała pytanie, czy generatory mogą być używane w programowaniu funkcjonalnym i czy posiadają stan.

Pete
źródło
2
Każdy program ma stan. Prawdziwe pytanie brzmi, czy kwalifikuje się jako stan funkcjonalny , który interpretuję jako „stan niezmienny”, który nie zmienia się po przypisaniu. Twierdzę, że jedynym sposobem na to, aby generator zwrócił coś innego przy każdym wywołaniu, jest włączenie stanu zmiennego.
Robert Harvey

Odpowiedzi:

14

Funkcje generatora nie są szczególnie szczególne. Możemy sami zaimplementować podobny mechanizm, przepisując funkcję generatora w stylu opartym na wywołaniu zwrotnym:

function downCounter(maxValue) {
  return {
    "value": maxValue,
    "next": function () {
      return downCounter(maxValue > 0 ? maxValue - 1 : 0);
     },
  };
}

let counter = downCounter(26);
counter.value; //=> 26
counter.next().value; //=> 25

Oczywiście downCounterjest tak czysty i funkcjonalny, jak to tylko możliwe. Tutaj nie ma problemu.

Protokół generatora używany przez JavaScript obejmuje obiekt zmienny. Nie jest to konieczne, patrz powyższy kod. W szczególności zmienne obiekty oznaczają, że tracimy przejrzystość referencyjną - możliwość zastąpienia wyrażenia jego wartością. Podczas gdy w moim przykładzie zawszecounter.next().value będzie oceniać bez względu na to, gdzie wystąpi i jak często go powtarzamy, nie jest tak w przypadku generatora JS - w pewnym momencie jest to i może być naprawdę dowolna liczba. Jest to problematyczne, jeśli przekazujemy odwołanie do generatora innej funkcji:252625

counter.next().value; //=> 25
otherFunction(counter); // does this consume the counter?
counter.next().value; // what will this be? It depends on the otherFunction()

Tak wyraźnie, generatory utrzymują stan i dlatego nie nadają się do „czystego” programowania funkcjonalnego. Na szczęście nie musisz robić czysto funkcjonalnego programowania i może być raczej pragmatyczny. Jeśli generatory sprawią, że Twój kod będzie wyraźniejszy, powinieneś go używać bez zbędnego sumienia. W końcu JavaScript nie jest czysto funkcjonalnym językiem, w przeciwieństwie do np. Haskell.

Nawiasem mówiąc, w Haskell nie ma różnicy między zwracaniem listy a generatorem, ponieważ używa on leniwej oceny:

downCounter :: Int -> [Int]
downCounter maxValue =
  maxValue : (downCounter (max 0 (maxValue - 1)))
-- invoke as "take n (downCounter 26)" to display n elements
amon
źródło