Это сообщение в блоге является частью продолжающейся серии, посвященной концепциям функционального программирования на JavaScript. Ранее мы обсуждали композицию, функторы, безточечный стиль и каррирование на простых примерах. Пожалуйста, обратитесь к части 1 и части 2, чтобы наверстать упущенное!
Теперь, в части 3, мы рассмотрим точечные функторы и представим новые функторы, которые позволят нам элегантно обрабатывать ошибки в наших композициях функций.
Disclaimer: Please note that I am not an authority on functional programming by any means. I intend this little series solely as “introduction to concepts”. This guide is quite unconventional in the order of the content. I choose to focus on practical examples, leaving essential theory such as typeclasses and function purity until late in the game. If you thirst for more technical resources, I highly recommend https://github.com/MostlyAdequate/mostly-adequate-guide, or you can also check out this free ebook on Haskell: https://learnyouahaskell.com/.
Помните, что функторы - это просто структуры данных, которые можно отображать. Другими словами, структура данных (или «контейнер») предоставляет интерфейс (мы называем его fmap
) для условного изменения значения, которое мы храним, и снова обертывает его для нас. Фактически, простой массив - это функтор: это структура данных, которую мы можем «сопоставить» (используя Array.map
).
Хорошо, давайте продолжим наше приключение!
Функтор гимнастики
Итак, у нас остался этот код из части 2:
Давайте проведем рефакторинг:
По сути, мы создали небольшую фабрику функторов. maybe
- это объект, который просто содержит функцию fmap
. В этом нет ничего страшного, поскольку он очень похож на наш оригинальный Maybe
( и суть того, что определяет функтор). На этом этапе ему просто не хватает внутренней ценности.
Более интригующим аспектом здесь может быть функция functor
, которая возвращает Object.assign
, которая объединяет переданные вами объекты в один. Вот и все. В этом случае первый параметр, который мы передаем, является новой копией maybe
, а второй параметр - литералом объекта, чтобы поместить значение, которое мы хотим сохранить внутри контейнера.
Object.assign
объединяет эти два объекта в один объект: функтор! Он работает так же, как и раньше, но теперь fmap
функтора существует только в прототипе. Все maybe
контейнеры объектов будут указывать на одну и ту же fmap
область памяти.
И последнее, но не менее важное: мы должны адаптировать способ создания функтора Maybe. Теперь у нас есть:
functor(maybe, “Doc Emmett Brown”);
вместо того:
Maybe(“Doc Emmet Brown”);
Заостренные функторы подкрадываются к нам
Этот синтаксис для создания функтора может показаться шагом назад. Так почему бы просто не добавить его к самому объекту Maybe? Давайте maybe
дадим новый метод и назовем его… of
. Взглянем:
Именно так мы добавили of
метод к нашему объекту Maybe. Не позволяйте тому факту, что of
вызывает maybe
внутри него, противоречит вам: maybe.of
просто отвечает за создание для нас нового функтора Maybe.
Мы можем вызвать maybe.of(“whatever we want”)
, чтобы поместить значение внутри контейнера Maybe, и мы можем с радостью начать «fmapping» над ним, то есть: условно изменить значение внутри него!
maybe.of(“cool”).fmap(val => val + “!”);
// «круто!»
Давайте перейдем к техническим вопросам. Благодаря этому of
методу у нас теперь явно есть так называемый точечный функтор. Это просто означает, что у нас есть возможность помещать любое значение (включая функции, как мы увидим в следующем посте) внутри контейнера.
В конце концов, мы создали тип возможно с методом of
, методом fmap
и значением. В конце концов, мы перестанем создавать собственные реализации функторов и будем использовать библиотеки (такие как Folktale), но важно понимать, что происходит за кулисами, по крайней мере, на высоком уровне.
Мои программы не безошибочны!
Контейнер maybe
хорош: он обрабатывает пустые данные и проверяет NULL за нас; но этого недостаточно. Совсем мало! Мы хотим учтиво выявлять ошибки и ошибки. Мы хотим получать обратную связь от нашей композиции, когда что-то идет не так.
Давайте освежим нашу память и взглянем на наш предыдущий пример, который заключался в получении первого инициала из строки человека:
Сейчас мы создаем пользователя с maybe.of(“Doc Emmett Brown”)
.
Давайте представим неверные данные. Предположим, какая-то неуклюжая душа или злой дьявол переходит в строку «Док Браун» в firstInitial
(без имени «Эммет»). Очевидно, это пройдет maybe
нулевую проверку, и в конце firstInitial
вернет “B”
. Хотя это не приводит к сбою нашей программы с ошибкой, это не то, что мы хотели! В этом случае мы хотим замкнуть композицию и вернуть другую строку вместе, например: “Not a valid string”
.
Теперь у нас есть два варианта вывода, с которыми нужно бороться: результат или сообщение об ошибке (игнорируя на мгновение возможное значение null).
Чтобы учесть это разделение в дороге, мы создадим два новых функтора: right
предоставит нам правильный результат, а left
будет отвечать за то, чтобы вежливо сообщить нам плохие новости о том, что что-то не так ... правильно.
На самом деле это просто продолжение того, что мы узнали о функторах. Давайте поговорим об этом:
Контейнер right
очень похож на контейнер возможно, но без нулевой проверки. Его fmap
метод применяет предоставленную функцию к значению внутри контейнера и помещает его обратно в right
контейнер для передачи.
left
container вообще ничего не делает! Его метод fmap
игнорирует переданную функцию и просто возвращает свой собственный контекст (this
); то есть сам контейнер без изменений. Все, что находится в left
контейнере, остается там без каких-либо изменений, несмотря ни на что. Это потому, что это будет наше сообщение об ошибке. Предположительно, мы не хотим ничего делать с нашим сообщением об ошибке, мы просто хотим знать, что оно говорит в конце.
Чтобы воспользоваться преимуществами наших новых функторов, мы адаптируем нашу getWords
функцию, чтобы она была более безопасной. Теперь он проверяет, содержит ли переданная строка три слова, как мы ожидали. Если это так, он конструирует right
контейнер с рассматриваемыми словами для передачи; в противном случае он создает left
контейнер с выбранным нами сообщением об ошибке.
Давайте рассмотрим код полностью, включая исходный maybe
:
Ну вот и все. Обратите внимание на то, что наша map
функция действительно очень мощная: ей все равно, какой функтор вы ей передаете, она просто использует соответствующий fmap
и передает новый контейнер вниз по композиции.
Вы можете представить себе, что в наши строительные блоки добавляется больше охранников.
Здесь следует отметить важное изменение: getWords
отличается тем, что теперь возвращает контейнер. Он проходит по right
или left
контейнеру внутри maybe
контейнера. Вот почему внутри нашей firstInitial
композиции мы должны быть осторожны при отображении этого нового контейнера.
R.pipe(getWords, map(getFirstName), map(getFirstLetter))
Для ясности: после того, как мы «открыли» контейнер maybe
и запустили getWords
, мы возвращаем либо контейнер right
, либо left
(а не просто значение), поэтому нам нужно map
перед применением следующей функции (s) в составе.
Позвольте мне отметить, что понятие правого / левого часто упоминается как либо, что мы не будем здесь реализовывать во всей его красе.
Наконец, что это за мерзость, когда мы хотим извлечь выгоду из контейнеров? Нам нужно .val
дважды (а иногда нам это нужно только один раз). Я не сказал вам, но получение значения и регистрация его волей-неволей - кощунство в функциональном программировании. Это «побочный эффект». Такая банальная вещь, как console.log, является побочным эффектом: она меняет состояние мира вне нашей функции. Это вне нашего контроля, и это опасно!
В части 4 мы познакомим вас с функтором IO, который с особой тщательностью предотвращает такие побочные эффекты. Вскоре после этого нам понадобятся монады, чтобы избавить нас от боли, связанной с вложенными контейнерами, с которыми мы уже сталкиваемся.
Следите за обновлениями!
Не стесняйтесь протестировать или разветвить код нашего последнего примера на CodePen.
Вы также можете подписаться на меня в твиттере :)
Спасибо за внимание!