Часть 2: Проблема мотивации в JS
Проблема мотивации
Ваш менеджер просит вас написать функцию countCharsOfFriendsPosts
, которая по идентификатору пользователя подсчитывала бы количество символов в сообщениях, сделанных друзьями пользователя. Ваш менеджер сообщает вам, что база данных пользователей будет представлена в виде массива в вашей локальной памяти, как показано ниже:
Будучи опытным функциональным программистом, вы быстро узнаете, что countCharsOfFriendsPosts
состоит из трех следующих более мелких функций:
getUserFriends
Имея идентификатор пользователя, запросите базу данных друзей пользователя.getUsersPosts
По заданному массиву пользователей запросите в базе данных сообщения, сделанные кем-либо из пользователей.countChars
Учитывая массив сообщений, подсчитайте количество символов в сообщениях.
Затем вы реализуете функции следующим образом:
И, наконец, у вас есть countCharsOfFriendsPosts
:
"Ух ты! Какая простая элегантность, — думаете вы про себя, — какой удивительный образец функционального программирования. Вы ожидаете, что ваш менеджер будет в восторге от ваших невероятных способностей к сочинению, однако находите его энтузиазма меньше, чем ожидалось. Он сообщает вам, что допустил ошибку в спецификациях, а база данных, в которой хранятся друзья пользователя, находится на удаленном сервере, и поэтому ее необходимо запрашивать асинхронно.
Ваш текущий код можно посмотреть и протестировать здесь: Ramda-Repl-1
Краткий (не совсем) обходной путь к истинному использованию функторов в FP
Ваш менеджер быстро убеждает вас, что это была единственная ошибка, которую он допустил в спецификации, и что сообщения пользователя действительно представлены в локальной памяти. Не испугавшись, вы переписываете getUserFriends
так, чтобы он возвращал обещание списка идентификаторов пользователей.
Однако вы быстро видите впереди блокпост; это полностью испортит вашу элегантную композицию! getUserFriendsAsync
возвращает Promise (List UserID), а getUserPosts
принимает List UserID. Типы не совпадают!
Отклонение 1: обещания, а потом
Прежде чем мы решим эту проблему, я хочу прояснить некоторую путаницу вокруг метода JS Promise then
. Функция, передаваемая методу then
, имеет две допустимые сигнатуры:
Чтобы избежать возможной путаницы, вместо использования метода then
я буду ссылаться на следующие две функции:
Sub-tour 2: Быстрое и грязное определение функтора
В будущем посте я подробно расскажу о полном определении, но пока это идеально:
Функтор F — это функция на функциях, такая, что для всех пар функций h и g:
1. Если h можно составить после g, то F(h) можно составить после F(g);
2. F(h) ∘ F(g) = F(h ∘ g).
В коде: F(c(h, g)) == c(F(h), F(g))
Другими словами, не имеет значения, составим ли мы сначала h ∘ g, а затем применим F, или применим F к каждому, а затем составим . Вот и все. Вот и все.
Вернемся к нашей истории…
Изображение составных функций
Через некоторое время вы понимаете, что здесь нужно сделать только одно: каким-то образом преобразовать композицию getUsersPosts
и countChars
и скомпоновать ее с getUserFriendsAsync
, чтобы получить новый countCharsOfFriendsPostsAsync
следующим образом:
Вот ссылка: Рамда-Репл-2
Хотя это решение не такое элегантное, как ваша первоначальная простая композиция, вы вполне удовлетворены этим решением. Вы уже собираетесь встать и передать это своему руководителю, но вместо этого видите, как он подбегает к вам. Пыхтя и пыхтя, он объясняет, что теперь, когда ваша функция стала асинхронной, высшее руководство настаивает на предоставлении ежедневного сообщения по умолчанию (DDP) для подсчета, если что-то пойдет не так при попытке получить сообщения друзей пользователя. DDP будет доступен в локальной памяти, и ваша программа должна считать символы DDP, если, к сожалению, что-то пойдет не так с получением постов друзей пользователя.
Вы быстро реализуете функцию перехвата с надеждой вставить ее там, где это необходимо для предоставления DDP, если произошла ошибка либо при получении друзей пользователя, либо при получении сообщений друзей.
Вы изучаете текущую конфигурацию —
pmap(countChars ∘ getUsersPosts) ∘ getUserFriendsAsync
— и поймите, что вам нужно поставить «catch
» после getUsersPosts
,, но перед countChars
. Начинаешь потеть, когда понимаешь, что опять типы не совпадают! pcatch принимает и возвращает promise массив строк, но тип возвращаемого значения getUsersPosts
и тип ввода countChars
— это просто массив строк!
Ты громко сетуешь, как если бы вместо
pmap(countChars ∘ getUsersPosts),
ты имел
pmap(countChars) ∘ pmap(getUsersPosts),
все ваши проблемы будут решены, так как тогда вы можете составить следующее:
Парень, сидящий рядом с вами, функциональный программист, шепчет вам на ухо «Promise — это функтор» (или, если он был математиком, возможно, «pmap — это функтор»). Вы мгновенно загораетесь. Чтобы pmap был функтором, это означало бы, что для любых функций g и f:
pmap(f ∘ g) = pmap(f) ∘ pmap(g)
И действительно, вы можете безопасно заменить pmap(countChars ∘ getUsersPosts) на pmap(countChars) ∘ pmap(getUsersPosts).
Благодаря функторам вы уверены в следующем коде:
Короче говоря, функторы отображают функции таким образом, что составление после отображения или отображение после составления дает один и тот же результат.
Посмотреть его можно здесь: Рамда-Репл-3
Далее: Проблема мотивации, продолжение
Ашер Кляйн — основатель и генеральный директор Descript Software. Он имеет степень в области чистой и прикладной математики Университета Конкордия.