Азбука теории категорий и функционального программирования

Функциональное программирование больше не просто академическое времяпрепровождение. Сообщество JavaScript также активизировало свою игру в этой области, и знание своей функциональной азбуки стало важной частью веб-разработки. Один из ключевых выводов изучения функционального программирования - это приспособление ума к возможностям и идеям за пределами ООП (объектно-ориентированного программирования). В этом тексте я попытаюсь раскрыть некоторые математические аспекты и идеи, лежащие в основе этих удобных функциональных концепций, и разрушить часть кирпичной стены академического жаргона, окружающего теоретические части функционального программирования.

Теория категорий - предметы и стрелки

Теория категорий обеспечивает основу для размышлений и обсуждения общих абстрактных структур в математике. Это три наиболее важных понятия: категория, функтор и естественное преобразование. Прежде чем углубляться в то, что это могло быть, давайте посмотрим на теорию категорий с более общей точки зрения. Разница между теорией категорий и ее соседом, теорией множеств, заключается в том, что в то время как теория множеств фокусируется на объектах, теория категорий описывает отношения между объектами. В этом случае объекты могут быть любыми, но если это упрощает, вы можете думать о них как о точках на графике. В этом случае ребра, соединяющие эти точки, вместе с точками образуют так называемую категорию. В теории категорий основное внимание уделяется этим краям или стрелкам между объектами. Они известны как морфизмы (или гомоморфизмы).

Суть категорий заключается в композициях внутри них. Композиция - это именно то, на что она похожа - вы сочиняете одну или несколько вещей. Ниже вы можете увидеть пример. Если есть стрелки между A и B и между B и C, стрелка от A до C указывает их состав. Так выглядит композиция морфизмов f и g в математике. Имя отображается над стрелкой, а в конечных точках отображаются объекты морфизмов.

Композиция в основном заключается в том, чтобы стрелки соединялись вместе, чтобы образовать более крупные стрелки. В программировании композиция будет означать составление функций. Например, у вас может быть функция, которая умножает на два, а другая - на 3. Каким бы способом вы их ни составили, если вы введете число 1, вы получите 6. Это также первое правило композиции в теории категорий: композиция ассоциативна, то есть порядок не имеет значения.

Второе правило состоит в том, что для каждого объекта A существует единичная стрелка (тождество на A). Это может показаться странным, но это означает только то, что функция или стрелка возвращает сам объект, с которым мы знакомы в программировании. В случае математических и натуральных чисел это будет добавление 0. Например, когда мы прибавляем 0 к 1, мы возвращаем 1. В теории категорий мы бы назвали это тождеством 1.

Теперь, когда мы увидели, как выглядят морфизмы (стрелки) и композиции, давайте подробнее рассмотрим понятие категории. Вы все еще можете думать об этом как о графике со связанными точками. Самый простой и очевидный вид категории - это пустая категория или null. Он не содержит предметов. Другой тип чрезвычайно распространенной категории - это моноид, который получается в результате сложения или умножения, например числа, множества и списки. Единственным критерием для моноида является то, что он должен быть набором с ассоциативной (в любом случае работает) бинарной операцией. Например, логическое значение может формировать моноиды с помощью таких операций, как True AND False. В этом случае True и False объединяются с помощью бинарного оператора AND и образуют моноид. Объединение строк - еще один пример создания моноида. Категории также могут быть так называемыми свободными категориями, которые состоят из объектов и морфизмов, но не являются моноидами.

Теперь, когда мы знаем, какой может быть категория и что она может делать, перейдем к функторам. Функтор - это отображение между категориями. Учитывая две категории, он отображает все объекты между ними, так что каждой точке в A соответствует точка в B. Он не только отображает объекты, но и сохраняет связи между ними. Если в A существует морфизм из a в b, под функтором F в категории B существует аналогичный морфизм, теперь известный как Fa в Fb. В программировании наиболее известен функтор map, который принимает объект и, сохраняя связи внутри него, сопоставляет его с другой категорией (или коллекцией, списком, массивом и т. Д.). Мы рассмотрим это на практике ниже.

Последняя сложная концепция, которую нам нужно решить, - это естественная трансформация. Естественное преобразование - это набор морфизмов, которые соединяют объекты с разными функторами. Например, у нас может быть категория C, которая содержит объект a и два функтора (G и F), идущие от C к D, которые оба производят Ga и Fa соответственно. Не кажется ли, что оба эти объекта, возникшие в результате, имеют некоторую связь? В самом деле, они это делают! Эти функторы связаны естественным преобразованием.

Самое лучшее в теории категорий - это то, что можно получить эти концептуальные инструменты для обсуждения абстрактных идей, связанных с функциональным дизайном. Он просто дает нам более широкую терминологию, которую мы можем использовать для описания структур, на которые мы смотрим (если мы не кодируем на Haskell, и в этом случае этот словарь является частью самого языка). С другой стороны, абстракция сама по себе не всегда добавляет ценности, что подчеркивал Норман Стинрод, называя теорию категорий «общей абстрактной чепухой». Как мы можем использовать эти идеи теории категорий в современном JavaScript?

Заставляем компьютер издавать звуковой сигнал с помощью теории категорий

Можно сказать, что история функционального программирования начинается с изобретения лямбда-исчисления в 1930-х годах. Хотя его цель заключалась в том, чтобы прояснить основы математики, теперь он известен как основа языков функционального программирования. Идея функционального программирования состоит в том, чтобы подходить к проблемам как к математическим функциям и избегать изменения состояния или мутации данных. В математике функция - это не что иное, как стрелка от a до b, без регистрации, что дает собаке cookie или другие побочные эффекты. Функциональное программирование является декларативным, а не императивным, что означает, что в нем используются декларирующие выражения, а не операторы. Это были громкие слова, но вот пример очень распространенной и простой проблемы, которую можно решить императивным (с помощью оператора) или декларативным (с помощью выражения) способом.

Благодаря отсутствию побочных эффектов функциональный код очень хорошо подходит для работы с параллелизмом. Это делает его подходящей парадигмой, когда нам нужно как можно быстрее обрабатывать большие объемы данных. Это также упрощает математическое выражение того, что вы хотите сделать, в коде.

Основные принципы

Функциональное программирование основано на идее использования чистых функций. Как мы выяснили, в математике все функции - это не что иное, как сопоставление значений. Чистая функция - это функция, которая при одном и том же вводе всегда возвращает один и тот же вывод. Следовательно, он не зависит от государства. У него никогда не должно быть побочных эффектов. Поскольку чистая функция не зависит от состояния, можно сказать, что она имеет ссылочную прозрачность, что означает, что она может быть заменена своим результатом. Например, selectSnack (дождь) всегда будет возвращать «горячий шоколад», поэтому вы можете заменить его этой строкой в ​​любое время.

Другой часто используемый термин - композиция, который относится к математической концепции композиции, которую мы обсуждали выше. Композиция просто означает объединение функций, чаще всего путем совмещения одной функции с другой. Это подводит нас к еще одному полезному способу размышления о категориях и возможности комбинирования: к типам. Типы подразумевают, что некоторые категории не могут быть составлены. Цель должна соответствовать источнику стрелки. Строго типизированные языки заставляют это делать, тогда как слабо типизированные, вероятно, каким-то образом будут ошибаться, когда это произойдет. В функцию, выполняющую действия со строкой, можно поместить только строку.

Развлечения с функторами

Как мы уже выяснили, функтор в математическом смысле - это карта между категориями. Несмотря на красивое звучание, большинство разработчиков уже используют функторы в своей повседневной работе. Наиболее часто используемый функтор в JavaScript - это map.

Как мы заявили выше, функтор отображает объекты одной категории в другую категорию, сохраняя порядок в такте. Например, в приведенном выше коде есть две закуски в категории закусок. В новой категории напитков есть соответствующие напитки каждой закуски.

Переходя к монадам

«О, вы знаете какой-нибудь Haskell? Я бы задал монаде вопрос, если бы сам знал ответ! » был комментарий, который я получил от технического интервьюера несколько месяцев назад. Он описывает подход к этой концепции в мире информатики: мистификация и неопровержимая. Теперь я попытаюсь раскрыть и упростить, о чем идет речь, хотя бы до некоторой степени. Монады - более мощные функторы, которые реализуют flatMap (тогда как функторы реализуют map). Разница между flatMap и map - это побочные эффекты, возникающие в flatMap. Что делает эту концепцию такой трудной для понимания, так это то, сколько вещей она может делать. Проще говоря, монада - это вещь, которая склеивает вещи. Говоря более техническим языком, он формирует композиции.

В чистом языке, таком как Haskell, использование монад - единственный способ делать вещи, вызывающие побочные эффекты, такие как ведение журнала или передача токена. Для этого нам нужен функтор, который выводит так называемую категорию Клейсли, которая состоит из самого функтора и его побочных эффектов (m). Если для каждого объекта есть композиция, которая является ассоциативной и имеет стрелку идентичности для каждого объекта, этот функтор m является монадой.

Какие преимущества мы можем получить, зная, что такое монады и как они работают? Во-первых, эта концепция дает нам основу для переосмысления программирования с помощью эффектов. Это также позволяет нам использовать эффекты на чистых функциональных языках, таких как Haskell. Одна из наиболее часто используемых монад в Haskell - это Maybe. Другой пример монады - List. По сути, это своего рода конструктор типа, который не имеет чистого возвращаемого типа. В JavaScript монады - это то, что вы можете нанести на карту. В случае с flatMap побочным эффектом сопоставления является сглаживание содержимого на один уровень. Когда map вернет функцию, flatMap может вернуть значение, созданное этой функцией.

Работа с теорией категорий

Теоретические части функционального программирования могут показаться утомительными и трудными для понимания из-за встроенной в них так называемой абстрактной общей бессмыслицы, но в повседневной жизни программистов они могут оказаться полезными для создания лучшего и более мощного кода. Такие концепции, как чистые функции и функторы, дают нам основу для оценки нашей работы и кода, который мы пишем. Часто ценится базовое владение этими концепциями, особенно в современном JavaScript. Я надеюсь, что этот текст помог упростить и демистифицировать по крайней мере некоторые аспекты теории категорий и концепций функционального программирования, чтобы в следующий раз, когда кто-либо, читающий его, попадет на техническое интервью, он не побоялся задать или ответить на «монадный вопрос».

Ресурсы:

Страна фантазий README

Монада - это просто моноид в категории эндофункторов

Функторы и моноиды (забавная функция)

Бартош Милевски - Программирование с математикой

Теория категорий NCatLab