Chociaż czasami jest to wyrażane w ten sposób, programowanie funkcjonalne¹ nie zapobiega stanowym obliczeniom. To, co robi, zmusza programistę do wyraźnego określenia stanu.
Na przykład, weźmy podstawową strukturę jakiegoś programu, używając kolejki rozkazującej (w pewnym pseudojęzycznym języku):
q := Queue.new();
while (true) {
if (Queue.is_empty(q)) {
Queue.add(q, producer());
} else {
consumer(Queue.take(q));
}
}
Odpowiednia struktura z funkcjonalną strukturą danych kolejki (wciąż w języku imperatywnym, aby rozwiązać jedną różnicę na raz) wyglądałaby następująco:
q := Queue.empty;
while (true) {
if (q = Queue.empty) {
q := Queue.add(q, producer());
} else {
(tail, element) := Queue.take(q);
consumer(element);
q := tail;
}
}
Ponieważ kolejka jest teraz niezmienna, sam obiekt się nie zmienia. W tym pseudokodzie q
sama jest zmienną; przypisania q := Queue.add(…)
i q := tail
niech wskazują na inny obiekt. Interfejs funkcji kolejki zmienił się: każda musi zwrócić nowy obiekt kolejki wynikający z operacji.
W języku czysto funkcjonalnym, tj. W języku bez skutków ubocznych, musisz wyraźnie zaznaczyć wszystkie państwa. Ponieważ producent i konsument prawdopodobnie coś robią, ich stan musi również znajdować się w interfejsie dzwoniącego.
main_loop(q, other_state) {
if (q = Queue.empty) {
let (new_state, element) = producer(other_state);
main_loop(Queue.add(q, element), new_state);
} else {
let (tail, element) = Queue.take(q);
let new_state = consumer(other_state, element);
main_loop(tail, new_state);
}
}
main_loop(Queue.empty, initial_state)
Zauważ, że teraz każdy stan jest jawnie zarządzany. Funkcje manipulowania kolejką przyjmują kolejkę jako dane wejściowe i tworzą nową kolejkę jako dane wyjściowe. Producent i konsument również przechodzą przez swój stan.
Programowanie współbieżne nie tak dobrze pasuje do wnętrza programowania funkcyjnego, ale bardzo dobrze pasuje wokół programowania funkcyjnego. Chodzi o to, aby uruchomić kilka oddzielnych węzłów obliczeniowych i pozwolić im wymieniać wiadomości. Każdy węzeł uruchamia program funkcjonalny, a jego stan zmienia się, gdy wysyła i odbiera komunikaty.
Kontynuując przykład, ponieważ istnieje jedna kolejka, jest ona zarządzana przez jeden konkretny węzeł. Konsumenci wysyłają do tego węzła komunikat w celu uzyskania elementu. Producenci wysyłają do tego węzła komunikat o dodaniu elementu.
main_loop(q) =
consumer->consume(q->take()) || q->add(producer->produce());
main_loop(q)
Jedynym „uprzemysłowionym” językiem, który ma właściwą współbieżność3 jest Erlang . Nauka języka Erlang jest zdecydowanie drogą do oświecenia⁴ na temat równoczesnego programowania.
Wszyscy teraz przechodzą na języki wolne od efektów ubocznych!
¹ Termin ten ma kilka znaczeń; tutaj myślę, że używasz go do programowania bez skutków ubocznych, i to jest znaczenie, którego również używam.
² Programowanie ze stanem niejawnym jest programowaniem imperatywnym ; orientacja obiektu jest kwestią całkowicie ortogonalną.
³ Zapalne, wiem, ale mam na myśli. Wątki z pamięcią współdzieloną to język asemblera współbieżnego programowania. Przekazywanie wiadomości jest znacznie łatwiejsze do zrozumienia, a brak efektów ubocznych naprawdę świeci, gdy tylko wprowadzisz współbieżność.
⁴ A pochodzi od kogoś, kto nie jest fanem Erlanga, ale z innych powodów.