Лекция
Привет, Вы узнаете о том , что такое чистота функции, Разберем основные их виды и особенности использования. Еще будет много подробных примеров и описаний. Для того чтобы лучше понимать что такое чистота функции, чистая функция , настоятельно рекомендую прочитать все из категории Функциональное программирование.
Мы упражнялись на машине родителей и не выезжали на автострады, пока в совершенстве не осваивали улицы родного района.
После множества практических занятий и нескольких щекотливых моментов, о которых наши родители хотели бы забыть, мы выучивались вождению и наконец получали свои водительские права.
С правами в руках мы садились за руль при любом удобном случае. С каждой новой поездкой наши навыки становились все лучше и лучше, уверенность росла. Затем наступал день, когда нам приходилось вести другую машину или наша собственная приказывала долго жить, и мы покупали новую.
Вспомните, на что был похож первый раз за рулем другой машины? Было ли это похоже на самый первый раз за рулем машины вообще? Даже не близко. В первый раз все было таким незнакомым. Конечно, мы сидели до этого в машине, но только в роли пассажира. На этот раз мы оказывались в сидении водителя. Один на один со всеми рычагами, кнопками и педалями.
Но когда мы вели нашу вторую машину, мы просто задавали себе несколько простых вопросов по типу: куда вставляется ключ, где переключаются фары ближнего и дальнего света, как использовать поворотники и как настроить зеркала заднего вида.
После всего этого мы вели свою машину как по маслу. Но почему в этот раз все было так просто по сравнению с первым разом?
Потому что новая машина была достаточно похожа на старую. Она имела все те базовые элементы, что нужны машине, и в большинстве случаев они находились на тех же местах, что и в старой.
несколько вещей были реализованы как-то иначе и, может быть, они имели какие-то дополнительные функциональные возможности, но мы и так не использовали их во всем нашем водительском опыте. Рано или поздно мы изучали все новые примочки. Как минимум, те, что нам реально требовались.
процесс изучения языков программирования похож на процесс обучения вождению. Первый раз — самый сложный. Но с багажом опыта за плечами все последующее обучение становится проще.
Когда вы начинаете изучать второй язык, вы спрашиваете себя: “Как мне создать модуль? Как реализовать поиск по массиву? Какие параметры принимает функция нахождения подстроки?”.
Вы уверены, что можете «научиться водить» на этом новом языке, потому что он напоминает вам о предыдущем языке, может, с несколькими новыми элементами, которые, надо надеяться, сделают вашу жизнь легче.
Ездили ли вы на одном автомобиле всю свою жизнь или на десятках автомобилях, представьте, что собираетесь сесть за штурвал космического корабля.
Если вы собираетесь летать на таком аппарате, то вряд ли будете надеяться, что навыки вождения на дороге как-то особенно вам помогут. Вы начнете все с нуля (Мы же программисты, черт возьми. Мы начинаем считать с нуля.).
Вы начнете свои тренировки с расчетом, что в космосе все работает иначе и что полеты на этой штуковине довольно отличны от вождения машины по земле.
Однако физика не изменилась. Путь, по которому вы двигаетесь, находится в пределах все той же Вселенной.
Такой же подход должен быть к изучению функционального программирования. Вы должны учитывать, что все будет по-другому. И то многое, что вы знали о программировании не будет перенесено в новую область.
Программировать — значит мыслить и функциональное программирование научит вас мыслить совсем по-другому настолько, что вы вероятно никогда не вернетесь назад на старый путь образа мышления.
Люди любят говорить эту фразу и в ней действительно есть доля правды. Изучать функциональное программированию — значит учить все с нуля. Не полностью, конечно, но фактически это так. В этой теме существует множество простых концепций, но вам лучше приготовиться к тому, что придется переучивать все.
С правильным подходом у вас будут правильные ожидания, а с правильными ожиданиями вы не захотите бросить дело, когда начнутся вещи по-тяжелее.
Есть также многие вещи, которые вы привыкли делать, как программист, но которые вы не сможете больше делать, занимаясь функциональным программированием.
Вспомните, как вы, чтобы выехать с проезжей части, давали задний ход на машине. Но на космическом корабле нет механизма реверса. Теперь вы должны подумать: “ЧТО? НЕТ ЗАДНЕГО ХОДА? КАК Я ДОЛЖЕН ВОДИТЬ БЕЗ ЗАДНЕГО ХОДА?”.
Что ж, оказывается вам не нужен реверс на космическом корабле, способном маневрировать в трехмерном пространстве космоса. Как только вы поймете это, вы больше не будете вспоминать о возможности заднего хода. И однажды, вы даже задумаетесь о том, насколько, на самом деле, ограничены обыкновенные машины.
Изучение функционального программирования требует времени. Запаситесь терпением.
Так что давайте покинем холодный мир императивного и медленно окунемся в горячие источники функционального программирования.
То, что следует в этой комплексной статье, — концепции функционального программирования, которые помогут вам перед полным погружением в первый функциональный язык. Или, если вы уже сделали решительный шаг в этой сфере, эти параграфы помогут заточить понимание идей.
Пожалуйста, не спешите. С этого момента не торопитесь и находите время, чтобы понять примеры кода. Лучше будет даже сделать небольшую паузу в прочтении после этой части статьи и дать озвученным идеям “устаканиться”. Затем возвращайтесь к чтению.
Самое главное — это то, чтобы вы поняли.
В языках программирования чистая функция — это функция, которая:
Наличия только одного из свойств недостаточно для того, чтобы функция была чистой.
Недетерминированность функции — возможность возвращения функцией разных значений несмотря на то, что ей передаются на вход одинаковые значения входных аргументов. Об этом говорит сайт https://intellect.icu . В этом случае невозможно построить однозначную таблицу значений функции; для таких функций таблицы значений выглядят как список (может быть, бесконечный) возможных значений, которые функция принимает на заданном наборе входных параметров.
Функция является детерминированной, если для одного и того же набора входных значений она возвращает одинаковый результат.
В императивных языках некоторые функции в процессе выполнения своих вычислений могут модифицировать значения глобальных переменных, осуществлять операции ввода-вывода, реагировать на исключительные ситуации, вызывая их обработчики. Такие функции называются функциями с побочными эффектами. Другим видом побочных эффектов является модификация переданных в функцию параметров (переменных), когда в процессе вычисления выходного значения функции изменяется и значение входного параметра.
Описывать функции без побочных эффектов позволяет практически любой язык программирования. Однако некоторые языки поощряют или даже требуют от некоторых видов функций использования побочных эффектов. Например, во многих объектно-ориентированных языках в функцию-член класса передается скрытый параметр — указатель на экземпляр класса, от имени которого вызывается соответствующая функция (например, в C++ этот параметр называется this, а в Object Pascal — self), который эта функция неявно модифицирует. Тем не менее, в языке C++ можно указать для метода класса модификатор const, тем самым сообщив компилятору о том, что метод не модифицирует данные класса.
Обычно функции, обладающие побочными эффектами, не являются детерминированными, поэтому функции без побочных эффектов, детерминированные функции и чистые функции иногда путают. В действительности это разные свойства функций. Например, функция rand, которая возвращает случайное число, или гипотетическая функция GetGlobalVarX, которая возвращает значение глобальной переменной X (и больше ничего не делает), не являются детерминированными, хотя они и не обладают побочными эффектами. А вот гипотетическая функция print, выводящая текст на экран и всегда возвращающая 0, наоборот — является детерминированной, но обладает побочным эффектом (вывод текста на экран). Ни одна из них не является чистой.
Если говорится о чистоте в функциональном программировании, значит подразумеваются чистые функции.
Чистые функции — очень простые. Они всего лишь производят операция над входными данными.
Вот пример чистой функции:
var z = 10; function add(x, y) { return x + y; }
Заметьте, что функция add не прикасается к переменной z. Она не читает ее значения и ничего не пишет в нее. Функция читает только x и y, свои входные данные, и возвращает результат их суммы.
Это и есть чистая функция. Если функция add имеет доступ к переменной z, она больше не может быть чистой.
Это пример другой чистой функции:
function justTen() { return 10; }
Если функция justTen чистая, она может возвращать только значение-константу. Почему?
Потому что мы не даем ей никаких входных данных. А значит, чтобы быть чистой, она не должна изменять никаких переменных, кроме тех, что были ей переданы. Единственное, что может возвратить такая функция — константа.
Пока функции, не принимающие параметров, не работают, они не очень полезны. Было бы лучше объявить justTen просто как константу.
Более полезные чистые функции принимают хотя бы один параметр.
Взгляните на этот пример:
function addNoReturn(x, y) { var z = x + y }
Посмотрите, эта функция ничего не возвращает. Она складывает x и y, записывает результат в переменную z, но не возвращает ее.
Эта чистая функция работает только с входными данными. Да, она выполняет сложение, но пока обратно возвращается ничего, функция бесполезна.
Все полезные чистые функции должны возвращать что-нибудь.
Давайте рассмотрим пример с первой функцией add еще раз:
function add(x, y) { return x + y; } console.log(add(1, 2)); // выводит 3 console.log(add(1, 2)); // все еще выводит 3 console.log(add(1, 2)); // БУДЕТ ВСЕГДА выводить 3
Обратите внимание, что add(1, 2) в результате всегда дает 3. Конечно, сюрприз не большой, но это потому что функция чистая. Если бы функция add брала значение откуда-то снаружи, вы бы никогда не могли наверняка предсказать ее поведение.
Чистая функция всегда возвращает одинаковые значения для одинаковых входных данных.
Поскольку чистые функции не могут изменять внешние переменные, все эти функции являются нечистыми:
writeFile(fileName); updateDatabaseTable(sqlCmd); sendAjaxRequest(ajaxRequest); openSocket(ipAddress);
Все функции в примере имеют то, что называют побочными эффектами. Когда Вы вызываете их, они меняют файлы и таблицы баз данных, отправляют данные на сервер или обращаются к операционной системе, чтобы получить сокет. Они делают куда больше, чем просто оперирование входными данными и возвращение значений. Следовательно, вы никогда не можете предсказать, что функция возвратит.
Чистые функции не имеют побочных эффектов.
В таких языках императивного программирования как JavaScript, Java и C# побочные эффекты везде. Это делает отладку проблематичной, потому что в коде Вашей программы переменная может быть изменена где угодно. В общем, если у Вас баг из-за переменой, принявшей неверное значение в неподходящее время, где Вы будете искать ошибку? Везде? Так дело не пойдет.
На этом месте, вы, вероятно, думаете: “КАК, ЧЁРТ ПОБЕРИ, Я СДЕЛАЮ ХОТЬ ЧТО-НИБУДЬ ОДНИМИ ТОЛЬКО ЧИСТЫМИ ФУНКЦИЯМИ?”.
В функциональном программировании вы не пишите только чистые функции.
Функциональные языки не могут исключить побочных эффектов, они могут только изолировать их. Пока у программ будут интерфейсы, взаимодействующие с реальным миром, некоторые части любой программы должны быть нечистыми. Цель — это свести к минимуму количество нечистого кода и отделить его от остальной части программы.
Вы помните, когда впервые увидели следующий код:
var x = 1; x = x + 1;
И тот, кто учил вас программированию, говорил забыть изученное на уроках математики. Ведь в математике x никогда не мог равняться x + 1.
Но в императивном программировании данный код означает «взять текущее значение x, прибавить к нему 1, положить результат обратно в x».
Что ж, в функциональном программировании выражение x = x + 1 недопустимо. Так что Вам надо вспомнить то, что вы забыли из математики... Если так можно выразиться.
В функциональном программировании нет переменных.
Сохраненные значения все еще называются переменными по историческим причинам, но они являются константами, то есть x, однажды приняв какое-либо значение, сохраняет его на всю жизнь.
Не волнуйтесь, x – это обычно локальная переменная, так что ее жизнь достаточно коротка. Но пока она жива, она никак не изменится.
Вот пример переменной-константы в Elm — чистом языке функционального программирования для web-разработки:
addOneToSum y z = let x = 1 in x + y + z
Если вы не знакомы с синтаксисом семейства языков программирования ML, позвольте мне объяснить. addOneToSum – это функция, принимающая 2 параметра: y и z.
Внутри блока let x приписывается значение 1, то есть он равен 1 до конца своей жизни. Его жизнь кончается, когда происходит выход из функции, или, более точно, когда исполняется блок let.
Внутри блока in вычисления могут включать значения, объявленные в блоке let, а именно: x. Возвращается результат вычисления x + y + z или, в точности, возвращается 1 + y + z, так как x = 1.
И снова я могу услышать, как вы вопрошаете: “КАК, ЧЕРТ ПОБЕРИ, Я ДОЛЖЕН СДЕЛАТЬ ХОТЬ ЧТО-НИБУДЬ БЕЗ ПЕРЕМЕННЫХ?!”.
Давайте подумаем, когда обычно мы хотим изменить переменную. Всего две основных причины, которые приходят на ум: многозначные изменения (например, изменение отдельного значения объекта или записи) и однозначные изменения (например, счетчики цикла).
Функциональное программирование решает проблему изменений значения записи, делая копию уже измененной записи. Это происходит оперативно, без копирования всех частей записи, используя определенные структуры данных, делающие это возможным.
Функциональное программирование решает также проблему однозначных изменений переменных, в сущности, тем же путем, просто делая их копию.
Да, кстати, и все это без циклов.
“СНАЧАЛА БЕЗ ПЕРЕМЕННЫХ, А ТЕПЕРЬ ЕЩЁ И БЕЗ ЦИКЛОВ? Я ТЕБЯ НЕНАВИЖУ!!!”
Попридержите коней. Это не значит, что мы не можем использовать циклы, просто здесь нет таких характерных операторов как for, while, do, repeat и так далее.
Функциональное программирование использует рекурсию для выполнения цикла.
Вот два примера реализации цикла в JavaScript.
// простой оператор цикла var acc = 0; for (var i = 1; i <= 10; ++i) acc += i; console.log(acc); // выводит 55// без оператора цикла или переменных (рекурсия) function sumRange(start, end, acc) { if (start > end) return acc; return sumRange(start + 1, end, acc + start) } console.log(sumRange(1, 10, 0)); // выводит 55
Обратите внимание, как рекурсия в функциональном подходе осуществляет то же самое, что и оператор цикла for, вызывая саму себя с новым параметром запуска (start + 1) и с новым счетчиком (acc + start). Она не изменяет старых значений. Вместо этого она использует новые значения, высчитанные из старых.
К сожалению, такие примеры не очевидны в JavaScript (даже если вы потратили некоторое время на их изучение) по двум причинам. Во-первых, синтаксис JavaScript засорен, а во-вторых, вы, вероятно, не привыкли думать рекурсивно.
Пример на языке Elm читать и, следовательно, понимать легче:
sumRange start end acc = if start > end then acc else sumRange (start + 1) end (acc + start)
Так этот код выполняется:
sumRange 1 10 0 = -- sumRange (1 + 1) 10 (0 + 1) sumRange 2 10 1 = -- sumRange (2 + 1) 10 (1 + 2) sumRange 3 10 3 = -- sumRange (3 + 1) 10 (3 + 3) sumRange 4 10 6 = -- sumRange (4 + 1) 10 (6 + 4) sumRange 5 10 10 = -- sumRange (5 + 1) 10 (10 + 5) sumRange 6 10 15 = -- sumRange (6 + 1) 10 (15 + 6) sumRange 7 10 21 = -- sumRange (7 + 1) 10 (21 + 7) sumRange 8 10 28 = -- sumRange (8 + 1) 10 (28 + 8) sumRange 9 10 36 = -- sumRange (9 + 1) 10 (36 + 9) sumRange 10 10 45 = -- sumRange (10 + 1) 10 (45 + 10) sumRange 11 10 55 = -- 11 > 10 => 55 55
Вам скорее всего кажется, что циклы for гораздо легче для понимания. Хотя это спорно и скорее всего является вопросом осведомленности, не-рекурсивные циклы подразумевают изменчивость, что по своей сути плохо.
Я не объясняю здесь преимущества использования парадигмы неизменяемости, но вы можете посмотреть параграф под названием Global Mutable State в статье Why Programmers Need Limits, если хотите изучить эту тему.
Одно очевидное преимущество — то, что если вы имеете доступ к какому-либо значению в вашей программе, это доступ только для чтения, а значит никто другой не может изменить это значение. Даже вы сами. Вследствие, никаких случайных изменений.
Также, если программа многопоточная, исполнение никакого потока не разрушит ваши планы. Поскольку значение — константа и если поток захочет изменить его, ему придется создать новое значение из старого.
Еще в середине девяностых я написал игровой движок для Creator Crunch и самый большой источник ошибок был связан с вопросом многопоточности. Я хотел бы знать про неизменяемость в то время. Но тогда меня больше волновала разница между двух и четырех скоростными приводами CD-ROM при игре.
Неизменяемость делает код проще и безопаснее.
Исследование, описанное в статье про чистота функции, подчеркивает ее значимость в современном мире. Надеюсь, что теперь ты понял что такое чистота функции, чистая функция и для чего все это нужно, а если не понял, или есть замечания, то не стесняйся, пиши или спрашивай в комментариях, с удовольствием отвечу. Для того чтобы глубже понять настоятельно рекомендую изучить всю информацию из категории Функциональное программирование
Комментарии
Оставить комментарий
Функциональное программирование
Термины: Функциональное программирование