Closures considered harmful - programowanie funkcyjne w js

0
Chet Corcos napisał(a):

Mathematicians like to describe programs as transformations of data which leads to the first concept — pure functions. Pure functions are functions without side-effects. Pure functions depend only on the inputs of the function, and the output should be the exact same for the same input.

Map, filter, reduce lub nawet głupie $().on(...) - w tych wszystkich przypadkach niejednokrotnie zdarzyło mi się korzystać ze zmiennych z zewnętrznego scope'a. Zwłaszcza przy dodawaniu listenerów, bo to na nich bardzo często jest tłumaczone jak działają domknięcia. Dopiero od jakiegoś czasu zacząłem mocno zwracać uwagę na to, czy moje funkcje faktycznie operują na tylko na parametrach do nich przekazywanych.

Czy jeżeli wyżej wymienionych przypadkach potrzebuję zmiennej z zewnętrznego scope'a to powinna mi się zapalić czerwona lampka i powinienem jeszcze raz przemyśleć to, jak napisałem dany fragment kodu?

A może HOF to the rescue?

const outerVariable = 1;

const data1 = [];
const data2 = data1.reduce((next, current) => {
  return current + outerVariable;
}, {});

// czy tak, jak by nie patrzeć też closure, ale...
const fn = variable => (next, current) => {
  return current + variable;
}

const outerVariable = 1;

const data1 = [];
const data2 = data1.reduce(fn(outerVariable), {});

Wołam @LukeJL @mr_jaro @Maciej Cąderek - zwłaszcza Twoje zdanie mnie interesuje, bo zdaje się, że kiedyś już mi zwróciłeś na to uwagę, a chciałbym się poprawić.

3

Moim zdaniem koleś trochę źle sformułował definicję czystych funkcji. W artykule podaje przykład, który ci powinien rozjaśnić o co mu naprawdę chodzi:

// pure function
const add10 = (a) => a + 10
// impure function due to external non-constants
let x = 10
const addx = (a) => a + x
// also impure due to side-effect
const setx = (v) => x = v 

Zwróć uwagę na wyrażenie impure function due to external non-constants. Ja bym definicję czystej funkcji nieco zmodyfikował - oprócz tego, że czyste funkcje nie mają efektów ubocznych to ich wynik może się zmieniać tylko wtedy, gdy zmieniają się argumenty wywołania.

Zewnętrzne stałe można by zamienić na zewnętrzne funkcje zwracające stałe bez straty ogólności. W zasadzie każda funkcja jest jakimś stanem. Stąd używanie zewnętrznych funkcji jest tak samo korzystaniem z zewnętrznego (dla domknięcia) środowiska jak używanie zewnętrznych stałych.

2

To się free variables i bound variables nazywa takie coś (jakbyś chciał szukać w google o tym).

Ale wydaje mi się, że zależy co chcesz zrobić, oba podejścia mają wady i zalety.

Jeśli funkcja nie korzysta z niczego, czego jej nie przekażesz, zachowuje prawo Demeter i w ogóle - to taką funkcję łatwiej jest potem wyjąć z jednego miejsca w kodzie i użyć w drugim itp. itd. Poza tym jest to bardziej elastyczne, możesz stworzyć 10 takich funkcji i każda będzie miała inny parametr.

Z drugiej strony nie zawsze jest to potrzebne, czasem użycie zewnętrznej zmiennej upraszcza co a przekazywanie jako parametr byłoby niepotrzebną komplikacją.

Idąc tropem, że wszystko ma być podawane do funkcji jako parametr musielibyśmy np. dojść do tego, że nie możemy używać żadnych bibliotek bezpośrednio - czyli np. chcesz użyć lodasha, to musisz go wstrzyknąć do funkcji i każda funkcja mogłaby wyglądać tak:

const foo = (arr, api) = {
   return api._.filter(arr, a => a < 3); 
};

co mogłoby się skończyć sporym overengineeringiem (tj. wstrzykiwanie bywa pożyteczne, ale wstrzykiwanie wszystkiego, żeby tylko nie użyć zewnętrznej stałej to przesada - domknięcia i zasięg leksykalny też są fajne przecież).

Dopiero od jakiegoś czasu zacząłem mocno zwracać uwagę na to, czy moje funkcje faktycznie operują na tylko na parametrach do nich przekazywanych.

To się da z automatu załatwić jakimś ESLintem (chociaż nie wiem czy jest gotowa reguła do tego, ale pewnie łatwo byłoby ją napisać, sam o tym myślałem).

2

@Desu

Oba podejścia są poprawne, tak jak napisał @LukeJL - zależy co chcesz osiągnąć - jak chcesz użyć konkretnej wartości w funkcji to pierwszy sposób jest tym czego potrzebujesz. Jak chcesz mieć fabrykę funkcji to uzywasz drugiej opcji. No ale jeśli chcesz wykorystać drugie rozwiązanie do pierwszego celu to jest to już nadmiarowe i przez to mniej czytelne (nie pokazujesz swojej intencji).

Co do "czystości" funkcji - mogłem gdzieś użyć uproszczenia, że funkcja powinna operować tylko na swoich parametrach (czy nawet przekłamania), natomiast z tego co pamiętam to tam gdzie Ci zwracałem uwagę chodziło o izolację kodu tak by można go było przetestować (był to inny przypadek niż tu omawiany).
Tak jak napisał @Wibowit - funkcja może jak najbardziej uzywać domknięcia i pozostać czysta, o ile robisz domknięcie na wartości lub jej odpowiedniku (niemutowalnej zmiennej, innej czystej funkcji).

Jedyne dwa warunki by funkcja była czysta to:

  • funkcja wywołana z takimi samymi argumentami zawsze zwraca tą samą wartość,
  • funkcja nie powoduje efektów ubocznych.

Jak widzisz nie ma tam nic o nieuzywaniu domknięć ;) Jak użyjesz ich tak by te dwa warunki zostały zachowane to wszystko jest ok.

EDIT:
Może jeszcze dodam, dla pełnej jasności:

Pure functions depend only on the inputs of the function

to co innego niż

Pure functions use only on the inputs of the function

Choć wiele osób pewnie powie, że to to samo. Dlatego warto pamiętać o dwóch zasadach, które przytoczyłem wyżej - one są jednoznaczne. Celem jest wyeliminowanie nielokalnego stanu (przynajmniej), pozbycie się czynnika czasu z naszego programu.

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