Функциональные практики и frontend: монады и функторы

    Всем привет! Меня зовут Дмитрий Руднев, я frontend-разработчик в компании БКС. Начинал я свой путь с верстки интерфейсов различной сложности и всегда уделял повышенное внимание именно интерфейсу: насколько пользователю будет комфортно с ним взаимодействовать, смог ли я донести до пользователя тот самый интерфейс, каким его задумал дизайнер.



    В этой серии статей я хочу поделиться своим опытом применения функциональных практик во frontend-разработке расскажу про плюсы и минусы, которые вы получите как разработчик, используя эти практики. Если тема вам понравится, то мы погрузимся в более «сухие» и сложные уголки функционального мира. Сразу отмечу, что пойдем мы от большего к меньшему, то есть посмотрим на классическое приложение c высоты птичьего полета, а по мере прохождения статей будем спускаться туда, где конкретная практика принесет нам заметную пользу.

    Итак, начнем с обработки состояний. Заодно расскажу, причем тут вообще монады и функторы.

    Intro


    При разгадке очередного интерфейса и нахождения точек соприкосновения между UI и аналитикой я стал замечать, что каждый раз, когда разработчик имеет дело с сетью, ему просто необходимо обрабатывать все состояния UI и описывать реакцию на то или иное состояние. А так как каждый из нас стремится к совершенству, то возникает желание под этот способ обработки состояний вывести какой-то паттерн, который максимально прозрачно опишет то, что вообще происходит и что является инициатором той или иной реакции, а как следствие — результатом работы. К счастью, в мире программирование почти всё, о чем вы можете подумать, было кем-то реализовано до вас.

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

    В нашем случае ситуация, которая возникает у большинства разработчиков, — обработка всех состояний UI-элемента и реакция на них. Проблема тут в том, что UI-элемент может взаимодействовать как с локальным состоянием (без выполнения асинхронных запросов), так и с удаленными ресурсами или хранилищами. Разработчики порой забывают обработать все краевые случаи, что приводит к неконсистентному поведению системы в целом.

    Все примеры будут содержать примеры кода с использованием библиотеки React и надмножеством языка JavaScript — TypeScript, а также библиотеки для функционального программирования fp-ts.

    Рассмотрим самый простой пример, где у нас есть список элементов, который мы запрашиваем с сервера, и нам нужно корректно отобразить UI в соответствии с результатом запроса. Нас интересует функция render, потому что в ней нам необходимо по ходу выполнения запроса отображать корректное состояние. Полный код примера можно посмотреть по ссылке: simple application. В дальнейшем там будет доступен полный проект, ориентированный на цикл статей, где по ходу мы будем разбирать отдельные его части.

     const renderInitial = (...) => ...;
     const renderPending = (...) => ...;
     const renderError = (...) => ... ;
     const renderSuccess = (...) => ... ;
    
    
    return (
       {state.subcribers.foldL(
         renderInitial,
         renderPending,
         renderError,
         renderSuccess,
       )}
    );
    

    На примере наглядно видно, что за каждое состояние модели данных отвечает своя функция, и каждая функция возвращает предписанный ей фрагмент UI (сообщение, кнопку и прочее). Забегая вперед, скажу, что в примере используется RemoteData monad.

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

    Functor и Monad


    А теперь начнем постепенно погружаться в прикладную теорию категорий и разберем такие понятия как Functor и Monad, а также рассмотрим практики по безопасной работе с данными, используя функциональные практики.

    «По существу, функтор является не более, чем структурой данных, позволяющей применять функции преобразования с целью извлечь значения из оболочки, модифицировать их, а затем поместить их обратно в оболочку.

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


    Эту цитату я взял из замечательной книги по рассмотрению методик функционального программирования на JavaScript. Начнем с теоретической составляющей и разберем, что на самом деле являет собой функтор. Для начала нам нужно на самом базовом уровне познакомиться с увлекательным разделом математики под названием теория категорий.

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

    Категория состоит из объектов и стрелок, которые направлены между ними. Легче всего представить категорию графически:

    Стрелки компонуются так, что если у вас есть стрелка от объекта А к объекту B и стрелка от объекта B к C, то должна быть и стрелка — их композиция от A к C. Думайте о стрелках как о функциях; ещё их называют морфизмами. У вас есть функция f , которая принимает в качестве аргумента A, а возвращает B. Ещё есть другая функция g, которая принимает в качестве аргумента B и возвращает C. Вы можете объединить их, передавая результат из f в g. Мы только что описали новую функцию, которая принимает A и возвращает C. В математике такая композиция обозначается небольшим кружком между обозначениями функций: g ◦ f. Обратите внимание на порядок композиции — справа налево.

    В математике композиция направлена справа налево. В этом случае помогает, если вы читаете g ◦ f как «g после f».

    -—объявление функции от A к B
    f :: A -> B
    
    
    -—объявление функции от B к С
    g :: B -> C
    
    
    -—Композиция A к C
    g . f
    

    Есть два очень важных свойства, которым композиция должны удовлетворять в любой категории.

    1. Композиция ассоциативна (ассоциативность — свойство операций, позволяющее восстановить последовательность их выполнения при отсутствии явных указаний на очерёдность при равном приоритете; при этом различается левая ассоциативность, при которой вычисление выражения происходит слева направо, и правая ассоциативность — справа налево. Соответствующие операторы называют левоассоциативными и правоассоциативными. Если у вас есть три морфизма (стрелки), f, g и h, которые могут быть скомпонованы (то есть их типы согласованы друг с другом) вам не нужны скобки чтобы сгруппировать их. Математически это записывается так h ◦ (g ◦ f) = (h ◦ g) ◦ f = h ◦ g ◦ f
    2. Для каждого объекта A есть стрелка, которая будет единицей композиции. Эта стрелка от объекта к самому себе. Быть единицей композиции — значит, что при композиции единицы с любой стрелкой, которая, либо начинается на A, либо заканчивается на A соответственно, композиция возвращает ту же стрелку. Единичная стрелка объекта A называется IDa (единица на A). В математической нотации, если f идет от A к B, то f ◦ idA = f

      Для работы с функциями единичная стрелка реализована в виде тождественной функции, которая просто возвращает свой аргумент.

    Теперь мы можем рассмотреть, что такое функтор в теории категорий.

    Функтор — особый тип отображений между категориями. Его можно понимать как отображение, сохраняющее структуру. Функторы между малыми категориями являются морфизмами в категории малых категорий. Совокупность всех категорий не является категорией в обычном смысле, так как совокупность её объектов не является классом.Википедия.

    Рассмотрим пример реализации функтора для контейнера Maybe, представляющего собой идею «значения, которое может отсутствовать».

    
    const compose = <A, B, C>(
       f: (a: A) => B,
       g: (b: B) => C,
     ): (a: A) => C => (a: A) => g(f(a));
    
    
    // Контейнер Maybe:
    type Nothing = Readonly<{ tag: 'Nothing' }>;
    type Just<A> = Readonly<{ tag: 'Just'; value: A }>;
    export type Maybe<A> = Nothing | Just<A>;
    
    const nothing: Nothing = { tag: 'Nothing' };
    const just = <A>(value: A): Just<A> => ({ tag: 'Just', value });
    
    // Функтор для контейнера Maybe:
    const fmap = <A, B>(f: (a: A) => B) =>
      (fa: Maybe<A>): Maybe<B> => {
        switch (fa.tag) {
          case 'Nothing':
            return nothing;
          case 'Just':
            return just(f(fa.value));
        }
    };
    
    // Закон 1: fmap id === id
    namespace Laws {
     console.log(
       fmap(id)(just(42)),
       id(just(42)),
     ); // => { tag: 'Just', value: 42 }
    
    
     // Закон 2: fmap f ◦ fmap g === fmap (f ◦ g)
     const f = (a: number): string => `Got ${a}!`;
     const g = (s: string): number => s.length;
     console.log(
       compose(fmap(f), fmap(g))(just(42)),
       fmap(compose(f, g))(just(42)),
     ); // => { tag: 'Just', value: 7 }
    }
    

    Метод fmap можно рассматривать с двух сторон:

    1. Как способ применить чистую функцию к «контейнеризированному» значению;
    2. Как способ «поднять в контекст контейнера» чистую функцию.

    Действительно, если немного по-другому расставить скобки в интерфейсе, мы можем получить такую сигнатуру функции fmap:

    const fmap: <A, B>(f: (a: A) => B) => ((ma: Maybe<A>) => Maybe<B>);
    

    Определив интерфейс:

    type Function1<Domain, Codomain> = (a: Domain) => Codomain;
    

    мы получим такое определение fmap:

    const fmap: <A, B>(f: (a: A) => B) => Function1<Maybe<A>, Maybe<B>>;
    

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

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

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

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

    А вот практические примеры применения данных свойств функтора и других теоретико-категорных конструкций я покажу в будущих статьях.
    ФГ БКС
    45,99
    Компания
    Поделиться публикацией

    Комментарии 13

      +6
      всегда уделял повышенное внимание именно интерфейсу

      Ваши бы декларативные ценности, да применительно к статье. Ибо вот это:


      А вот практические примеры применения данных свойств функтора и других теоретико-категорных конструкций я покажу в будущих статьях.

      Ну очень плохой интерфейс с читателем. Этот как "Loading..." с неопределённым горизонтом.

        +7

        Если бы я не знал всё то, о чём тут написано, я бы ничего не понял.


        Что это за метод foldL, выглядящий как будто это и не foldL вовсе, а match какой-нибудь? В исходном коде так вовсе используется какой-то левый пакет @devexperts/remote-data-ts, хотя статья вроде как про fp-ts!


        По существу, функтор является не более, чем структурой данных, позволяющей применять функции преобразования с целью извлечь значения из оболочки, модифицировать их, а затем поместить их обратно в оболочку

        Написано так, как будто функтор — это какая-то конкретная структура данных, а не класс структур данных. Правильное определение должно начинаться как-то так: "Функтором называется любая структура данных, позволяющая..."


        ассоциативность — свойство операций, позволяющее восстановить последовательность их выполнения при отсутствии явных указаний на очерёдность при равном приоритете; при этом различается левая ассоциативность, при которой вычисление выражения происходит слева направо, и правая ассоциативность — справа налево

        Смешались в кучу математика и синтаксис языка. Под ассоциативностью операции в данном случае понимается именно вот это свойство: h ◦ (g ◦ f) = (h ◦ g) ◦ f. К левой или правой ассоциативности оно не имеет отношения.


        Как ни странно, но отличать левую ассоциативность от правой обычно требуется лишь для неассоциативных операций!


        Для каждого объекта A есть стрелка, которая будет единицей композиции.

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


        Теперь мы можем рассмотреть, что такое функтор в теории категорий. Функтор — особый тип отображений между категориями.

        Вот как раз теперь, после всех этих композиций, появилось ощущение, что функтор — это одна из стрелок на диаграмме выше. Что, разумеется, не так: функторов на той диаграмме вообще нет (ну, если только там не нарисована категория малых категорий)


        Учитывая, что в этом определении вообще нет упоминания композиции, совершенно не понятно зачем размещать его именно тут. Кстати, определения тут как такового нет.


        Функторы между малыми категориями являются морфизмами в категории малых категорий.

        Отлично! Тихой сапой появилось сразу два новых понятия: морфизм (если кто не знает — это как раз те стрелочки на диаграмме выше) и "малая категория". Хорошо хоть, дальше эти понятия не используются.


        Метод fmap можно рассматривать с двух сторон

        Во-первых, это у вас вовсе не метод. Во-вторых, откуда он вообще взялся? Нет, ну я-то знаю, что fmap — основная операция для функтора, но в статье она вылезла из ниоткуда.


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

        Это уже ребус какой-то. Даже известная шутка про "всего лишь моноид в категории эндофункторов" — и то понятнее как определение монады.


        Для того чтобы лучше понять монады, необходимо усвоить следующие важные понятия: Монада. Предоставляет абстрактный интерфейс для монадических операций

        Ага, а сепулька предоставляет абстрактный интерфейс для сепуления!


        Монадический тип. Конкретная реализация данного интерфейса

        А это вообще ошибка. Монадический тип — это любой тип, имеющий kind * -> *. То есть это даже более слабое свойство, чем у функтора!

        +1

        Я один не понимаю зачем тянуть функциональщину в языки для нее не предназначенные?

          +1
          Потому что некоторые языки ни для чего не предназначены, а функциональщина позволяет хоть как-то их использовать более-менее эффективно.
            +1

            Что значит "эффективно"?
            Кстати много языков сейчас имеет синтаксис, облегчающий использование монад. Но JS\TS в их число не входит.

              0
              Значит писать позволяет писать программы не тратя слищком много времени на отладку и стыковку разных компонент.
                –1

                Есть какое-то исследование что монады и ФП помогают совершать меньше ошибок? Или значительно сокращают объем кода? Многие вещи в JS\TS уже достаточно функциональны — массивы, продолжения.
                Зачем лепить еще что-то?

                –1
                Кстати много языков сейчас имеет синтаксис, облегчающий использование монад. Но JS\TS в их число не входит.

                Входит. Синтаксис генераторов — монадический.

                  0

                  Но генератор нельзя применять с произвольной монадой.

                    0

                    Ну, почти с произвольной. С-но, какие вы знаете полезные монады кроме list и подобных, которые нельзя реализовать через генераторы?

            0

            Очень меметичный пост. 10 буррито из 10.

              +1
              В исходном коде так вовсе используется какой-то левый пакет @devexperts/remote-data-ts, хотя статья вроде как про fp-ts!

              Этот пакет входит в экосистему fp-ts (Heavily based on fp-ts lib)

              Написано так, как будто функтор — это какая-то конкретная структура данных, а не класс структур данных. Правильное определение должно начинаться как-то так: «Функтором называется любая структура данных, позволяющая...»

              Вы правы, моя формулировка не совсем корректная, спасибо за замечание! (Поправлю)

              Смешались в кучу математика и синтаксис языка. Под ассоциативностью операции в данном случае понимается именно вот это свойство: h ◦ (g ◦ f) = (h ◦ g) ◦ f. К левой или правой ассоциативности оно не имеет отношения.

              Здесь ассоциативность была затронута для пояснения, а не для примера, поэтому как вы заметили (затронутое в статье свойство, к левой или правой ассоциативности не имеет отношения)

              Вы уверены, что это вообще для обычных фронтендеров написано? Вся моя любовь к математике не помешала мне прочитать «единицу композиции» в значении «неделимый элемент», по аналогии с «единицей развёртывания» или там с «единицей функциональности».

              В этом цикле статей я хотел попробовать донести для «обычных фронтендеров», что применение функциональных практик возможно, в том числе и при разработке интерфейсов.

              Отлично! Тихой сапой появилось сразу два новых понятия: морфизм (если кто не знает — это как раз те стрелочки на диаграмме выше) и «малая категория». Хорошо хоть, дальше эти понятия не используются.

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

              Во-первых, это у вас вовсе не метод. Во-вторых, откуда он вообще взялся? Нет, ну я-то знаю, что fmap — основная операция для функтора, но в статье она вылезла из ниоткуда.

              Вы правы, это не метод, это функция. Моя ошибка — поправлю и заодно поясню.

              Это уже ребус какой-то. Даже известная шутка про «всего лишь моноид в категории эндофункторов» — и то понятнее как определение монады.

              Я поправлю данное пояснение. (Спасибо за замечание)

              А это вообще ошибка. Монадический тип — это любой тип, имеющий kind * -> *. То есть это даже более слабое свойство, чем у функтора!


              Я употребил слова «монадический тип» в значении — «тип, для которого можно определить экземпляр монады» Пометку обновлю, спасибо за замечание.

              В целом, спасибо вам за комментарии!

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

              Самое читаемое