Помните: функциональное программирование — это не программирование с помощью функций! ! !
23.4 Функциональное программирование
23.4.1 Что такое функциональное программирование
? Если вы спросите так прямо, вы обнаружите, что эту концепцию нелегко объяснить. Многие ветераны с многолетним опытом работы в области программирования не могут внятно объяснить, что изучает функциональное программирование. Функциональное программирование действительно является незнакомой областью для программистов, знакомых с процедурным программированием. Понятия замыкания, продолжения и каррирования кажутся нам совершенно незнакомыми. Знакомые if, else и while не имеют ничего общего. Хотя функциональное программирование имеет прекрасные математические прототипы, с которыми не может сравниться процедурное программирование, оно настолько загадочно, что освоить его могут только люди с докторской степенью.
Совет: этот раздел немного сложен, но это не обязательный навык для освоения JavaScript, если вы не хотите использовать JavaScript для выполнения задач, выполняемых в Lisp, или не хотите изучать эзотерические навыки работы с ним. функциональное программирование, вы можете пропустить его и перейти к следующей главе своего путешествия.
Итак, вернемся к вопросу: что такое функциональное программирование? Ответ длинный…
Первый закон функционального программирования: функции относятся к первому типу.
Как следует понимать само это предложение? Что такое настоящий Тип Один? Давайте рассмотрим следующие математические понятия:
двоичное уравнение F(x, y) = 0, x, y — переменные, запишите его как y = f(x), x — параметр, y — возвращаемое значение, f — от x. to y Отношение отображения называется функцией. Если да, G(x, y, z) = 0 или z = g(x, y), g — это отношение отображения x, y в z, а также функция. Если параметры x и y g удовлетворяют предыдущему соотношению y = f(x), то мы получаем z = g(x, y) = g(x, f(x)). Это имеет два значения. x) — функция от x и параметр функции g. Во-вторых, g — функция более высокого порядка, чем f.
Таким образом, мы используем z = g(x, f(x)) для представления связанного решения уравнений F(x, y) = 0 и G(x, y, z) = 0, которое является итерационной функцией . Мы также можем выразить g в другой форме, вспомнив z = g(x, y, f), так что мы обобщаем функцию g до функции более высокого порядка. По сравнению с предыдущим, преимущество последнего представления состоит в том, что это более общая модель, такая как ассоциированное решение T(x,y) = 0 и G(x,y,z) = 0. Мы также можно выразить в той же форме (только пусть f=t). В этой языковой системе, поддерживающей итерацию преобразования решения задачи в функцию высшего порядка, функция называется «первым типом».
Функции в JavaScript явно относятся к «первому типу». Вот типичный пример:
Array.prototype.each = функция (замыкание)
{
вернуть this.length ? [закрытие(это[0])].concat(this.slice(1).each(закрытие)) : [];
}
Это действительно волшебный волшебный код, раскрывающий всю прелесть функционального стиля. Весь код состоит только из функций и символов. Он прост по форме и бесконечно эффективен.
[1,2,3,4].each(function(x){return x * 2}) получает [2,4,6,8], а [1,2,3,4].each(function(x ){return x-1}) получает [0,1,2,3].
Суть функциональной и объектно-ориентированной заключается в том, что «Дао следует природе». Если объектно-ориентированное — это моделирование реального мира, то функциональное выражение — это моделирование математического мира. В некотором смысле его уровень абстракции выше, чем у объектно-ориентированного, поскольку математические системы по своей сути обладают несравнимыми по своей природе особенностями. абстракции.
Второй закон функционального программирования: замыкания — лучшие друзья функционального программирования.
Замыкания, как мы объясняли в предыдущих главах, очень важны для функционального программирования. Его самая большая особенность заключается в том, что вы можете напрямую получить доступ к внешней среде из внутреннего уровня, не передавая переменные (символы). Это очень удобно для функциональных программ при множественной вложенности. Вот пример:
(функция externalFun(x)
{
возвращающая функция InternalFun(y)
{
вернуть х * у;
}
})(2)(3);
Третий закон функционального программирования: функции могут быть карринговыми.
Что такое карринг? Это интересная концепция. Начнем с математики: скажем, рассмотрим трехмерное пространственное уравнение F(x, y, z) = 0, если ограничить z = 0, то получим F(x, y, 0) = 0, обозначаемое как F '(х, у). Здесь F' — очевидно, новое уравнение, представляющее собой двумерную проекцию трехмерной пространственной кривой F(x, y, z) на плоскость z = 0. Обозначим y = f(x, z), пусть z = 0, получим y = f(x, 0), обозначим его как y = f'(x), будем говорить, что функция f' является решением Карри функции f .
Пример каррирования в JavaScript приведен ниже:
функция add(x, y)
{
if(x!=null && y!=null) return x + y;
иначе if(x!=null && y==null) возвращает функцию (y)
{
вернуть х + у;
}
иначе if(x==null && y!=null) возвращает функцию (x)
{
вернуть х + у;
}
}
вар а = добавить (3, 4);
вар б = добавить (2);
var c = b(10);
В приведенном выше примере b=add(2) приводит к функции карринга add(), которая является функцией параметра y, когда x = 2. Обратите внимание, что она также используется выше. замыканий.
Интересно, что мы можем обобщить карринг для любой функции, например:
function Foo(x, y, z, w)
{
вар args = аргументы;
если (Foo.length < args.length)
функция возврата()
{
возвращаться
args.callee.apply(Array.apply([], args).concat(Array.apply([], аргументы)));
}
еще
вернуть x + y – z * w;
}
Четвертый закон функционального программирования: отложенная оценка и продолжение.
//TODO: подумайте об этом еще раз здесь
23.4.2 Преимущества
модульного тестирования
функционального программированияКаждый символ строгого функционального программирования является ссылкой на прямую величину или результат выражения, и ни одна функция не имеет побочных эффектов. Потому что значение нигде не изменяется, и ни одна функция не изменяет величину за пределами своей области действия, которая используется другими функциями (например, членами класса или глобальными переменными). Это означает, что результатом вычисления функции является только ее возвращаемое значение, и единственное, что влияет на ее возвращаемое значение, — это параметры функции.
Это мечта юнит-тестера. Для каждой функции в тестируемой программе вам нужно заботиться только о ее параметрах, без необходимости учитывать порядок вызовов функций или тщательно устанавливать внешнее состояние. Все, что вам нужно сделать, это передать параметры, которые представляют крайние случаи. Если каждая функция программы проходит модульное тестирование, вы можете быть уверены в качестве программного обеспечения. Но императивное программирование не может быть таким оптимистичным. В Java или C++ недостаточно просто проверить возвращаемое значение функции — мы также должны проверить внешнее состояние, которое функция могла изменить.
Отладка
Если функциональная программа ведет себя не так, как вы ожидаете, отладка — это пустяк. Поскольку ошибки в функциональных программах не зависят от путей кода, не связанных с ними до выполнения, проблемы, с которыми вы можете столкнуться, всегда можно воспроизвести. В императивных программах ошибки появляются и исчезают, поскольку работа функции зависит от побочных эффектов других функций, и можно долго искать в направлениях, не связанных с возникновением ошибки, но безрезультатно. С функциональными программами дело обстоит иначе: если результат функции неправильный, то, что бы вы еще ни выполняли до этого, функция всегда будет возвращать один и тот же неверный результат.
Как только вы воссоздадите проблему, найти ее первопричину не составит труда и, возможно, даже сделает вас счастливым. Прервите выполнение этой программы и проверьте стек. Как и в императивном программировании, вам будут представлены параметры каждого вызова функции в стеке. Но в императивных программах этих параметров недостаточно. Функции также зависят от переменных-членов, глобальных переменных и состояния класса (которое, в свою очередь, зависит от многих из них). В функциональном программировании функция зависит только от своих параметров, и эта информация находится прямо у вас перед глазами! Кроме того, в императивной программе простая проверка возвращаемого значения функции не может гарантировать, что функция работает правильно. Для подтверждения необходимо проверить состояние десятков объектов за пределами области действия этой функции. В функциональной программе все, что вам нужно сделать, это посмотреть на ее возвращаемое значение!
Проверьте параметры и возвращаемые значения функции по стеку. Как только вы обнаружите необоснованный результат, введите эту функцию и следуйте ей шаг за шагом. Повторяйте этот процесс, пока не найдете точку, в которой генерируется ошибка.
Параллельные функциональные программы могут выполняться параллельно без каких-либо изменений. Не беспокойтесь о взаимоблокировках и критических разделах, потому что вы никогда не используете блокировки! Никакие данные в функциональной программе не изменяются дважды в одном и том же потоке, не говоря уже о двух разных потоках. Это означает, что потоки можно просто добавлять, не задумываясь, не вызывая традиционных проблем, от которых страдают параллельные приложения.
Если это так, то почему не все используют функциональное программирование в приложениях, требующих высокопараллельных операций? Ну, они это делают. Эрикссон разработал функциональный язык Erlang и использовал его в телекоммуникационных коммутаторах, требующих чрезвычайно высокой отказоустойчивости и масштабируемости. Многие люди также открыли для себя преимущества Erlang и начали его использовать. Мы говорим о системах управления телекоммуникациями, которые требуют гораздо большей надежности и масштабируемости, чем типичная система, разработанная для Уолл-стрит. На самом деле система Erlang не является надежной и расширяемой, в отличие от JavaScript. Системы Erlang просто надежны.
На этом история параллелизма не заканчивается. Даже если ваша программа является однопоточной, функциональный компилятор программы все равно может оптимизировать ее для работы на нескольких процессорах. Посмотрите на следующий код:
String s1 = someLongOperation1();
Строка s2 = someLongOperation2();
String s3 = concatenate(s1, s2);
В функциональном языке программирования компилятор анализирует код для выявления потенциально трудоемких функций, создающих строки s1 и s2, а затем запускает их параллельно. Это невозможно в императивных языках, где каждая функция может изменять состояние вне области действия функции, а последующие функции могут зависеть от этих модификаций. В функциональных языках автоматически анализировать функции и выявлять кандидатов, подходящих для параллельного выполнения, так же просто, как автоматическое встраивание функций! В этом смысле функциональное программирование является «приверженным будущему» (даже если мне не нравится использовать отраслевые термины, на этот раз я сделаю исключение). Производители оборудования больше не могли заставить процессоры работать быстрее, поэтому они увеличили скорость процессорных ядер и добились четырехкратного увеличения скорости за счет параллелизма. Конечно, они также забыли упомянуть, что дополнительные деньги, которые мы потратили, были потрачены только на программное обеспечение для решения параллельных задач. Небольшая часть императивного программного обеспечения и 100% функциональное программное обеспечение могут работать на этих машинах параллельно.
Раньше горячее развертывание кода
требовало установки обновлений Windows, а перезагрузка компьютера была неизбежна, причем не один раз, даже если была установлена новая версия медиаплеера. Windows XP значительно улучшила эту ситуацию, но она все еще не идеальна (сегодня я запустил Центр обновления Windows на работе, и теперь в трее всегда появляется раздражающий значок, если я не перезагружаю компьютер). Системы Unix всегда работают в лучшем режиме. При установке обновлений необходимо останавливать только системные компоненты, а не всю операционную систему. Несмотря на это, это все еще неудовлетворительно для крупномасштабного серверного приложения. Телекоммуникационные системы должны быть работоспособными 100% времени, потому что если во время обновления системы произойдет сбой экстренного набора, это может привести к гибели людей. Нет никаких причин, по которым фирмам с Уолл-стрит приходится отключать сервисы на выходных, чтобы установить обновления.
Идеальная ситуация — обновить соответствующий код, вообще не останавливая работу каких-либо компонентов системы. Это невозможно в императивном мире. Учтите, что когда среда выполнения загружает класс Java и переопределяет новое определение, все экземпляры этого класса будут недоступны, поскольку их сохраненное состояние теряется. Мы могли бы начать писать какой-нибудь утомительный код контроля версий, чтобы решить эту проблему, затем сериализовать все экземпляры этого класса, уничтожить эти экземпляры, затем воссоздать эти экземпляры с новым определением этого класса, а затем загрузить ранее сериализованные данные и надеяться, что загрузка код правильно перенесет эти данные в новый экземпляр. Кроме того, код портирования необходимо переписывать вручную для каждого обновления и проявлять большую осторожность, чтобы не допустить нарушения взаимосвязей между объектами. Теория проста, но практика непроста.
Для функциональных программ все состояние, то есть параметры, передаваемые функции, сохраняются в стеке, что упрощает горячее развертывание! Фактически, все, что нам нужно сделать, это провести разницу между рабочим кодом и новой версией, а затем развернуть новый код. Остальное сделает автоматически языковой инструмент! Если вы думаете, что это научная фантастика, подумайте еще раз. В течение многих лет инженеры Erlang обновляли свои работающие системы, не прерывая их работу.
Машинное рассуждение и оптимизация
Интересным свойством функциональных языков является то, что их можно рассуждать математически. Поскольку функциональный язык — это всего лишь реализация формальной системы, все операции, выполненные на бумаге, могут быть применены к программам, написанным на этом языке. Компиляторы могут использовать математическую теорию для преобразования фрагмента кода в эквивалентный, но более эффективный код [7]. Реляционные базы данных подвергаются подобной оптимизации уже много лет. Нет причин, по которым этот метод нельзя было бы применить к обычному программному обеспечению.
Кроме того, вы можете использовать эти методы, чтобы доказать правильность частей вашей программы и, возможно, даже создать инструменты для анализа вашего кода и автоматического создания крайних случаев для модульного тестирования! Эта функциональность не имеет никакой ценности для надежной системы, но если вы разрабатываете кардиостимулятор или систему управления воздушным движением, этот инструмент незаменим. Если приложения, которые вы пишете, не являются основными задачами в отрасли, этот тип инструментов также может дать вам козырную карту перед конкурентами.
23.4.3 Недостатки функционального программирования
Побочные эффекты замыканий
В нестрогом функциональном программировании замыкания могут переопределять внешнюю среду (мы уже видели это в предыдущей главе), что приводит к побочным эффектам, и когда такие побочные эффекты возникают часто. среда, в которой работает программа, часто меняется, ошибки становится трудно отслеживать.
//TODO:
рекурсивная форма
Хотя рекурсия зачастую является наиболее лаконичной формой выражения, она не так интуитивно понятна, как нерекурсивные циклы.
//TODO:
слабость отложенного значения
//TODO: