Хотя иногда это выражается таким образом, функциональное программирование не мешает вычислениям с состоянием. Что он делает, так это заставляет программиста сделать состояние явным.
Например, давайте возьмем базовую структуру некоторой программы, использующей императивную очередь (в некотором псевдоязыке):
q := Queue.new();
while (true) {
if (Queue.is_empty(q)) {
Queue.add(q, producer());
} else {
consumer(Queue.take(q));
}
}
Соответствующая структура с функциональной структурой данных очереди (все еще на императивном языке, чтобы справляться с одним различием за раз) будет выглядеть следующим образом:
q := Queue.empty;
while (true) {
if (q = Queue.empty) {
q := Queue.add(q, producer());
} else {
(tail, element) := Queue.take(q);
consumer(element);
q := tail;
}
}
Поскольку очередь теперь неизменна, сам объект не изменяется. В этом псевдокоде q
сама переменная; назначения q := Queue.add(…)
и q := tail
сделать его указывать на другой объект. Интерфейс функций очереди изменился: каждый из них должен возвращать новый объект очереди, полученный в результате операции.
В чисто функциональном языке, то есть в языке без побочных эффектов, вам нужно сделать все состояния явными. Поскольку производитель и потребитель, по-видимому, что-то делают, их состояние должно быть и в интерфейсе вызывающего абонента.
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)
Обратите внимание, что теперь каждый элемент состояния управляется явно. Функции управления очередью принимают очередь в качестве входных данных и создают новую очередь в качестве выходных данных. Производитель и потребитель также проходят через свое состояние.
Параллельное программирование не очень хорошо вписывается в функциональное программирование, но оно очень хорошо вписывается в функциональное программирование. Идея состоит в том, чтобы запустить несколько отдельных вычислительных узлов и позволить им обмениваться сообщениями. Каждый узел запускает функциональную программу, и его состояние изменяется при отправке и получении сообщений.
Продолжая пример, поскольку существует одна очередь, она управляется одним конкретным узлом. Потребители отправляют этому узлу сообщение для получения элемента. Производители отправляют этому узлу сообщение для добавления элемента.
main_loop(q) =
consumer->consume(q->take()) || q->add(producer->produce());
main_loop(q)
Единственный «промышленно развитый» язык, который получает право параллелизма3 - это Erlang . Изучение Эрланга - это определенно путь к просветлению о параллельном программировании.
Теперь все переходят на языки без побочных эффектов!
Term этот термин имеет несколько значений; здесь я думаю, что вы используете это для обозначения программирования без побочных эффектов, и это значение я тоже использую.
² Программирование с неявным состоянием является обязательным программированием ; ориентация объекта является полностью ортогональной проблемой.
³ Воспалительно, я знаю, но я это имею в виду. Потоки с общей памятью - это язык ассемблера параллельного программирования. Передача сообщений намного легче понять, и отсутствие побочных эффектов действительно сияет, как только вы вводите параллелизм.
⁴ И это от кого-то, кто не фанат Эрланга, но по другим причинам.