Перевод терминов функционального программирования
Термины должны выбираться такими чтобы не требовать объяснения. Необходимо обращать внимание на то как называют такие понятия опытные программисты когда объясняют "на пальцах" новичкам.
- монада - list - контейнер
- map() - преобразовать
- функтор - обработчик
Содержание
Рабочие варианты терминов
- list - Список, контейнер
- map - Прим (применить функцию), преобразовать
- car - ПЭЛ (первый элемент), первый
- cdr - ОЭЛ (остальные элементы), остаток
Перевод терминов из книги О хаскеле по человечески
- Ленивые вычисления - отложенные вычисления
- Булева переменная - логическая переменная
- Паттерн матчинг - сравнение с/по образцом/у
из статьи адаптированный текст
Теперь наконец попробуем разобраться в терминах Теории Категорий на понятных условному сишнику/джависту примерах.
Категория – тип данных - любой примитивный или составной: строка, число, пара строка-число (кортеж), массив чисел, тип функций (например, функция IntToStr имеет тип Целый -> Строка). Функциональные типы (т.е. сигнатуры) – полноценные типы. Можно из них тоже собрать кортеж или сложить в массив. Параметры обобщенных типов (те, которые с дженериками, т.е. в Array[Int], например, Array – это обобщённый тип, а Int – это его параметр) еще могут быть Ковариантными/Контравариантными/Инвариантными. Эта тема стоит отдельной статьи.
Важное уточнение: данное тут определение очень вольное. Категория — понятие еще более абстрактное, чем тип. Но для начала понимания, давайте примем пока это упрощение.
Морфизм – преобразователь типов - это любая функция, преобразующая один тип в другой. Преобразователь типов IntToStr – это морфизм. Итого: видим «морфизм» — читаем «функция конвертации». «Эндоморфизм» — это преобразователь типов (морфизм) внутри типа данных (категории), т.е. преобразование типа в самого себя. Функция «синус» - Эндоморфизм из Типа Данных (Категории) Double в нее же, хотя и крайне примитивный. Более сложный пример преобразователь типов (морфизма): преобразователь пары строк (username, password) в объект сессии.
Монада – это простой контейнер. Ее основная цель – обрабатывать данные в контейнере, не вынимая их наружу. Для этого к ней прицепили парочку функций Преобразовать (map). Например, если у нас есть монада-список (массив) чисел, то преобразование их в строки можно сделать прямо в массиве, сразу получив на выходе готовый массив строк, не заморачиваясь с циклами, созданием новых массивов и т.п.
Важное уточнение: когда я говорю «превратили числа в строки, не доставая из контейнера», я не имею в виду, что поменялось содержимое самого массива. Исходный массив (экземпляр) остается неизменным, но вызвав преобразование, мы получим второй массив (или дерево, или любой другой контейнер) идентичной структуры, только уже содержащий строки.
Но это только половина правды. Вторая половина состоит в том, что когда вы получили указатель на массив строк, никакого массива еще нет. Все вычисления "отложенные/ленивые". Это означает, что пока вы не попытаетесь прочитать что-то из этого "массива" (который на самом деле просто аналог сишного Handle) ничего выполнено и сконвертировано не будет. Поэтому вы можете строить цепочки преобразований, которые мгновенно возвращают управление (потому что ничего не делают), и в конце, когда вам понадобится что-то достать из конечного контейнера, только тогда вся цепочка и раскрутится в последовательность вызовов конкретных преобразователей типов - ЦелоеВСтроку и им подобным.
В хаскеле функция такой обработки называется bind (>>=), что имеет корни в Теории Категорий. Ведь bind – это «связывание», т.е. функция bind фактически создает ребро в графе категорий (связывает узлы). В большинстве языков эта функция называется map() (строго говоря, flatMap) («отобразить», «поставить в соответствие»). По мне, логичнее было бы ее назвать cast() («снять слепок», «преобразовать»), но меня почему-то не спросили.
Есть распространённая монада Option/Maybe, смысл которой в том, чтобы хранить одно единственное значение. Или не хранить. Например, мы могли бы сделать функцию StrToIntOption, которая бы принимала строку и возвращала Option[Int], т.е. такой Контейнер (монаду), в которой либо лежало бы число (если строка в него преобразуется (парсится)), либо не содержало бы ничего. С таким контейнером мы можем делать разные вещи, даже не проверяя, что в нем лежит. Например, можем умножить его содержимое на «2», взять синус, вывести на экран или отправить по сети. Для этого мы используем наш метод Преобразовать (map()), передав в него в качестве параметра функцию, которая должна сделать что-то полезное. Но фактически выполнена эта функция будет только, если в контейнере значение правда лежит (число преобразовалось (распарсилось)). Если в контейнере ничего нет, то ничего и не произойдет, ничего не умножится, ничего не отправится.
А вообще полезных монад люди придумали множество. Но все они несут один простой смысл, который описан выше. В любой большой системе можно наковырять с десяток служебных типов, которые можно было бы заменить монадическим типом. Монада-контейнер может накапливать в себе любой контекст, произошедшие ошибки, логирование и что угодно еще, не останавливая поток обработки и не засоряя код ненужным бойлер-плейтом. С помощью монад довольно элегантно решается большинство задач Аспектно-ориентированного программирования. Мощь и удобство функции map() оказались настолько велики, что ее добавили к себе многие современные языки, далекие от чистого ФП.
Функтор — это та самая упомянутая выше функция map/bind. Смысл названия «Функтор»: «функция над функциями», но он не отражает ее сути. Суть же – взять какой-нибудь преобразователь типов (морфизм) и применить его прямо внутри контейнера (монады). Т.е. Функтор – это преобразование (морфизм) в контейнере (монаде). Функтор выглядит со стороны это как будто вызвали функцию преобразовать ("map"), передав в качестве параметра другую функцию - преобразователь типов (ЦелоеВСтроку), а в результате она вернет нам такой же массив, только уже со строками вместо чисел.
Теперь вернемся к теме «монада является функтором». На практике это означает, что в классе монады есть метод Применить (map). Все. Но по смыслу монада – это контейнер, а функтор – это оператор преобразования содержимого.
У Функтора есть двоюродный брат – Аппликативный функтор.
Аппликативный функтор – ОтложенноеПреобразование?/ПреобразоватьПозже?? - те же яйца, только к нему добавили еще одну фичу, чтобы конвертацию можно было делать отложено в некоторых условиях, когда хотят скомпоновать содержимое нескольких контейнеров, опять же ничего не извлекая. Например (Option[username], Option[password]) -> Option[(username, password)]). С функцией Применить (map()) мы бы не смогли сделать такую пару, не извлекая самих значений (нам сначала бы пришлось получить логин и пароль, а потом сложить в новый контейнер их пару). Поэтому тут добавляется еще одна функция ap() (от apply), которая «отложено/лениво» преобразует данные (как делал ее брат — Функтор) только когда кто-то начнет читать результирующий контейнер. На практике она возвращает частично примененную функцию – это ту, которая…
Частично примененные функции, Каррирование. Объяснение на простом примере функции с двумя переменными: давайте подставим в нее первый параметр, а второй оставим пока неизвестным. По факту получим функцию с одним параметром. Вот и все, мы только что сделали «каррирование» функции из арности k=2 в k=1. На самом деле в Хаскеле, например, вообще нет понятия количества параметров у функции в том смысле, как это делается в си-подобных языках. Например, если функции измерения расстояния надо 3 координаты (имеет сигнатуру Double -> Double -> Double -> Double), то мы можем в выражениях использовать ее как с одним, так и с двумя или с тремя параметрами. Отличия будут в типах возвращаемых результатов. В случае, если мы передадим все координаты, то она вернет «Double», если передадим на одну координату меньше – она вернет Double -> Double, т.е. функцию от одного параметра Double, если мы передадим всего одну координату, то результат будет иметь вид Double -> Double -> Double (функция от двух параметров Double, возвращающая Double).
А если мы такую же логику применим к обобщенным типам (дженерикам), т.е. рассмотрим некий тип F[T1, T2, T3], то окажется что у такого типа есть конструктор, дающий конкретные реализации обобщенного типа (например F[Int, Double, String]). У этого конструктора будет 3 аргумента: T1, T2, T3. Действовать с ними он будет ровно так же, как вышеописанная функция. Т.е. его тоже можно "каррировать", уменьшая количество параметров, передавая часть из них. Только вот в этом случае не говорят о арности, а говорят о разных "кайндах" (kind). Почему? Потому что гладиолус.
Лямбда выражения и Замыкания. Лямбда исчисление имеет к ФП такое отношение, как Теория Категорий, т.е. никакое. Просто люди, привнесшие эту концепцию в ФП, были прожжёнными математиками, и дали ей такое название. Для того чтобы понять суть «лямбд» и «замыканий» не нужна высшая математика. Лямбда-выражение – это просто анонимная функция. Когда у тебя есть язык, весь состоящий из функций, и когда функции можно передавать в качестве значений другим функциям, то не очень хочется для каждой такой функции придумывать имя. Особенно если эта функция состоит из одной строки и тройки-другой слов.
Эффект – это один из столпов ФП, наравне с Контейнером (монадой) (и настолько же абстрактен, как она). Эффект – это алгоритмическая (императивная) часть программы. Любой программе, написанной на чистом и няшном ФП, приходится взаимодействовать с внешним миром. Любое взаимодействие заставляется выйти из теплого мирка контейнеров-монад в грязный реальный алгоритмический (императивный) мир и что-то вывести на экран, что-то принять по сети, прочитать текущее время и т.п. Кроме того любое извлечение данных из контейнера – это Эффект (т.к. с извлечением может быть запущена отложенная реальная обработка данных). Чтобы вывести распарсенное число на экран, нам придется-таки узнать, а было ли оно вообще распарсено (извлечь содержимое Option/Maybe). Не удивительно, что функциональщики стараются держать Эффекты под контролем. Весь прикол функционального мира состоит в том, что Эффекты до самого последнего момента тоже остаются монадными (т.е. упакованными в свой контейнер эффектов). Если где-то в коде ФЯП написано, что надо что-то вывести в консоль, то оно (текст) будет упаковано в монаду и доставлено вверх по кол-стеку прямо в функцию main. Функция main возвращает именно такую супер-монаду IO (а не void как в «сях»), которая собрала в себя всю логику программы, и все эффекты ввода-вывода в консоль. Только внутренний boot-код, сгенерированный компилятором, запустит исполнение Эффекта (извлечение контейнера IO) – откроет ящик Пандоры, из которого выскочат все реальные строки, вычисленные тут же «на лету» цепочками различных преобразований.
Эффект – это, на самом деле, венец всего ФП, после понимания которого наступает долгожданный катарсис «я наконец-то понял!».
Что дальше
Я надеюсь, что мое объяснение было полезным и дало вам привязку мира ФП к реальным задачам. Поэтому если вы еще не начали, то попробуйте начать писать функциональный код. Вот прямо сразу, на том языке, на котором вы пишете все время. Как я упоминал выше, это можно делать почти в любом ЯП. Для этого надо всего лишь стараться максимально следовать следующим принципам:
Писать чистые функции – функции, которые оперируют только теми данными, которые получили на входе, никак их не меняя и возвращая обработанный результат. Не использовать глобальные переменные и другие хранилища состояния в процессе обработки – выполнять Эффекты только в самом конце работы логики. Аккуратнее с ООП. Изменяемые Объекты – это глобальные переменные. Старайтесь по возможности использовать immutable структуры данных. Если ваш ЯП уже содержит функции map() и различные вариации монад (Option, Try и т.п.) старайтесь использовать их по максимуму. В следующий раз попробуйте вместо цикла for написать map/forEach/fold/reduce или использовать другой Функтор, подходящей сигнатуры. Нет подходящего? Напиши его!
Заключение
Аппетит приходит во время еды. Постепенно развивая в себе функциональное чутье, со временем вы постепенно начнете «видеть» монады. Ваш код станет выразительнее, компактнее, надежнее. Но есть один недостаток: взглянув через год на свой код вам станет нестерпимо стыдно и захочется переписать его заново вдвое короче. По крайней мере так было у меня.
Ссылки
- https://habr.com/ru/post/505928/ почему функциональное программирование такое сложное