Выделение действий, вычислений и данных #
Васильев Андрей Михайлович, 2022
Версии презентации
План лекции #
- Определение различий между действиями, вычислениями и данными
- Получение навыка разделения кода на категории при обдумывании задачи, при написании кода и при чтении существующего кода
- Отслеживание распространения действий по кодовой базе
- Нахождение действий в существующем исходном коде
Свойства действий, вычислений и данных #
Действия | Вычисления | Данные |
---|---|---|
Зависят от количества вызовов и времени | Вычисляют значение на основе входных данных | Факты о событиях |
Также называются: функции с побочными эффектами, нечистые функции | Также называются: чистые функции, математические функции | |
Примеры: отправка почты, чтение из базы данных | Примеры: Нахождение максимального числа, проверка корректности почтового адреса | Примеры: почтовый адрес, который ввёл пользователь, стоимость доллара, прочитанного из API банка |
Деятельность при разработке ПО #
- Обдумывание проблемы
- Действия требуют специального отношения?
- Какие события необходимо сохранить в данных?
- Какие решения следует описать в вычислениях?
- Написание приложения
- Явное распределение кода по категориям
- Рефакторинг действий в вычисления, вычислений в данные
- Чтение кода
- Выявление действий в коде, так как они несут опасности
Вычисления являются ссылочно прозрачными #
Запрос к вычислению может быть заменён данными
Например сложение, +
, является вычислением. Вычисление 3 + 2
можно заменить числом 5
Категоризировать можно любой код #
Рассмотрим процесс совершения покупок в виде диаграммы временной последовательности
- Проверка холодильника — это действие, так как зависит от времени, когда мы заглядываем в холодильник
- Поездка в магазин — это действие, так как на две поездки потребуется дважды больше топлива
- Покупка продуктов — это действие, так как количество продуктов в магазине изменяется
- Поездка домой — это действие, так как невозможно вернуться домой находясь дома
Все шаги являются действиями, но где же остальные типы кода?
Детальный анализ шагов #
Проверка холодильника #
Зависит от времени проверки холодильника, однако список продуктов в холодильнике — это данные, назовём их текущим складским запасом
Поездка в магазин #
Поездка в магазин является действием, которое опирается на множество данных: местоположение магазина, маршрут и т.д.
Покупка продуктов #
Самый простой алгоритм для выполнения покупок: составить список покупок и набрать продуктов согласно ему
необходимый запас - текущий складской запас = список покупок
Поездка домой #
Как и поездка в магазин является действием, которое мы также не будем разделять на части
Расширенная последовательность покупки #
Данную модель можно далее детальнее разбирать на типы, делая её более глубокой
- Проверка холодильника может быть разделена на проверку холодильника и проверку морозильника
- Покупка продуктов может быть разделена на добавление в корзинку и оплату на кассе
Резюме #
- Разбиение на действия, вычисления и данные можно применить к любой задаче.
- Действия могут скрывать за собой другие действия, вычисления и данные. Необходимо детально разбираться с действиями, чтобы уменьшать их
- Вычисления могут состоять из небольших вычислений и данных.
- Данные могут состоять только из других данных.
- Вычисления часто не обособляются. Их можно выделить с помощью наблюдений:
- принимаются ли какие-то решения?
- можно ли что-то спланировать заранее?
Данные #
Что такое данные? #
Данные содержат факты о произошедших событиях
Как данные реализуются в коде? #
В языке JavaScript можно использовать встроенные типы данных: числа, строки, массивы и объекты
В других языках, например в Haskell, можно определить свои типы, описывающие предметную область
Как данные записывают знания? #
Данные сохраняют знания в своей структуре. Структура данных должна соответствовать предметной области, в которой оперирует приложение
Неизменяемость #
Для реализации неизменяемости данных применяются подходы:
- Копирование при записи, создаётся копия данных перед её модификацией
- Защитное копирование, сохранение данных, которые могут быть изменены
Преимущества данных #
Данные наиболее ценны тем, что мы не можем с ними сделать: их невозможно выполнить, изменить. В результате они понятны
- Данные можно сериализовать и десериализовать для
- передачи на другой компьютер
- сохранения на жёсткий диск для дальнейшего чтения
- сохранения на долгий срок
- Данные можно сравнивать на равенство
- Данные открыты для интерпретации
Вычисления и действия не обладают этими достоинствами
Недостатки данных #
Открытость для интерпретации является также и недостатком: их необходимо интерпретировать, чтобы данные стали полезными
Вычисления могут быть выполнены и могут быть полезны, даже если их трудно понять
Данным требуется вычисления для их интерпретации
Одним из ключевых навыков программиста на функциональных языках является способность представления данных, чтобы их можно было интерпретировать сейчас и заново интерпретировать в будущем
Ещё про данные #
Все ли данные содержат описания фактов о произошедших событиях? Что же тогда делать с фактами о человеке или другой сущности?
Данные приходят в информационную систему в определённое время
Имя человека может попасть систему через оправку формы регистрации на веб-странице приложения. Данный запрос был обработан, интерпретирован и нужные данные были сохранены в базу данных
Их можно интерпретировать как имя человека, но они появились в результате события — отправки запроса
Полезные свойства определения данных как фактов о событиях:
- Указывает на то, что данные необходимо интерпретировать
- Данные используются в информационных системах, которые получают и обрабатывают информацию
Применение функционального мышления при написании кода #
Рассмотрим систему по раздаче скидочных купонов. Люди могут подписаться на интернет-рассылку и раз в неделю получать на почту сообщение с актуальными купонами
Компания решила реализовать новую систему привлечения новых клиентов: если человек порекомендует систему 10 своим знакомым, то они будут получать лучшие купоны
Структура базы данных #
Таблица пользователей
rec_count | |
---|---|
lena@mail.ru | 2 |
pasha@yandex.ru | 16 |
ivan@example.com | 1 |
dima@bz.com | 0 |
tanja@lol.com | 25 |
bot@botnet.com | 0 |
Таблица купонов
coupon | rank |
---|---|
MAYDISCOUNT | good |
10PERCENT | bad |
PROMOTION45 | best |
IHEARTYOU | bad |
GETADEAL | best |
ILIKEDISCOUNTS | good |
Планирование изменений #
Команда разработчиков составила список шагов, которые необходимо выполнить, чтобы реализовать новую стратегию
- Отправить почтовое сообщение
- Прочитать список подписчиков из базы данных
- Ранг каждого купона
- Прочитать купоны из базы данных
- Тема почтового сообщения
- Почтовый адрес
- Количество рекомендаций
- Выяснение кому какое письмо следует передать
- Запись подписчика
- Запись купона
- Список записей купонов
- Список записей подписчиков
- Тело письма
Категоризация элементов решения #
Определим категорию для каждого из элемента решения
- Отправить почтовое сообщение, действие
- Прочитать список подписчиков из базы данных, действие
- Ранг каждого купона, данные
- Прочитать купоны из базы данных, действие
- Тема почтового сообщения, данные
- Почтовый адрес, данные
- Количество рекомендаций, данные
- Выяснение кому какое письмо следует передать, вычисление
- Запись подписчика, данные
- Запись купона, данные
- Список записей купонов, данные
- Список записей подписчиков, данные
- Тело письма, данные
Отправка почтовых сообщений #
Рассмотрим один из подходов к решению поставленной задачи отправки разных почтовых сообщений в зависимости от количества рекомендаций
№1 получение списка подписчиков из БД #
Получение списка является действием, т.к. зависит от времени, когда мы выполняем запрос к базе данных
Полученный список подписчиков является данными
№2 Получение списка купонов из БД #
Получение купонов тоже является действием. Купоны в базе данных постоянно изменяются.
Полученный список купонов является данными
№3 Формирования списка почтовых сообщений для отправки #
В рамках данного шага будут созданы новые данные. Этот подход часто используется при использовании ФЯП: данные создаются отдельно от места их дальнейшего использования
Такой же подход использовался ранее, когда мы вычисляли список покупок до поездки в магазин
№4 Отправка почтовых сообщений #
После формирования списка сообщений для отправки осталось только лишь выполнить данный план
Формирование писем #
Рассмотрим в деталях процесс формирования текстов писем для отправки
Шаг №1: Формирования списков купонов #
Шаг №2: Определение типа письма для подписчика #
Шаг №3: Формирование письма #
Полный взгляд на вычисление #
Реализация процесса отправки почтовых сообщений #
После формирования плана перейдём к реализации отправки почтовых сообщений
Начнём с представления подписчика, будем использовать объекты JavaScript
var subscriber = {
email: "pavel@mail.ru",
rec_count: 16
}
Для представления качества купона будем использовать строки
var rank1 = "best";
var rank2 = "good";
Определение качества купона для подписчика #
Задача определения качества купона — это вычисление, которое можно удобно реализовать с помощью функции
function subscriberCouponRank(subscriber) {
if(subscriber.rec_count >= 10) {
return "best";
} else {
return "good";
}
}
Данную функция не содержит побочных эффектов
Формирование списка купонов #
Для представления купона будем использовать объекты JavaScript:
var coupon = {
coupon: "10PERCENT",
rank: "bad"
}
Реализуем функцию для формирования списка купонов по рангу
function selectCouponsByRank(coupons, rank) {
var result = [];
for(var i = 0; i < coupons.length; i++) {
var coupon = coupons[i];
if(coupon.rank === rank) {
result.push(coupon);
}
}
return result;
}
Формирование почтовых сообщений #
Для представления почтового сообщения тоже используем объекты:
var message = {
from: "newsletter@coupondog.co",
to: "user@mail.ru",
subject: "Ваши купоны на эту неделю",
body: "Здравствуйте. Ваши купоны на эту неделю..."
}
Формирование почтового сообщения #
function emailForSubscriber(subsriber, goods, bests) {
var rank = subscriberCouponRank(subscriber);
if(rank === "best") {
return {
from: "newsletter@coupondog.co",
to: subscriber.email,
subject: "Ваши лучшие купоны на эту неделю",
body: "Ваши лучшие купоны: " + bests.join(", ")
};
} else {
return {
from: "newsletter@coupondog.co",
to: subscrber.email,
subject: "Ваши хорошие купоны на эту неделю",
body: "Ваши хорошие купоны: " + goods.join(", ")
};
}
}
Планирование всех почтовых сообщений #
Используя предыдущие функции (вычисления) сформируем список сообщений для всех подписчиков
function emailsForSubscribers(subscribers, goods, bests) {
var emails = [];
for(var i = 0; i < subscribers.length; i++) {
var subscriber = subscribers[i];
var email = emailForSubscriber(subscriber, goods,
bests);
emails.push(email);
}
return emails;
}
Данная функция является вычислением, т.к. зависит только от аргументов и не формирует побочные эффекты
Отправка сообщения #
Рассмотрим вариант реализации отправки почтовых сообщений
function sendCoupons() {
var coupons = fetchCouponsFromDB();
var goodCoupons = selectCouponsByRank(coupons, "good");
var bestCoupons = selectCouponsByRank(coupons, "best");
var subscribers = fetchSubscribersFromDB();
var emails = emailsForSubscribers(subscribers,
goodCoupons, bestCoupons);
for(var i = 0; i < emails.length; i++) {
var email = emails[i];
emailSystem.send(email);
}
}
Порядок реализации функционала #
- Сначала были реализованы структуры данных, которые будут использованы в рамках проекта
- Затем были сформированы функции для вычисления новых данных на основании изначальных данных
- В конце они были объединены в рамках функций-действий
Такой подход к решению задачи часто используется программистами на функциональных языках
Вопрос про нагрузку #
В текущей реализации мы формируем почтовые сообщения перед отправкой. Что делать в случае, когда подписчиков будет несколько миллионов?
Правильный ответ: изначально это неизвестно, всё будет сильно зависеть от среды исполнения нашего кода и её возможностей
В качестве решения данной проблемы в рамках представленной реализации можно разбить всех пользователей на части
function sendCoupons() {
var coupons = fetchCouponsFromDB();
var goodCoupons = selectCouponsByRank(coupons, "good");
var bestCoupons = selectCouponsByRank(coupons, "best");
var page = 0;
var subscribers = fetchSubscribersFromDB(page);
while(subscribers.length > 0) {
var emails = emailsForSubscribers(subscribers,
goodCoupons, bestCoupons);
for(var i = 0; i < emails.length; i++) {
var email = emails[i];
emailSystem.send(email);
}
page++;
subscribers = fetchSubscribersFromDB(page);
}
}
Рассмотрение свойств вычислений #
Вычисления — это операции по формированию результатов в зависимости от входящих значений. Не зависят от времени запуска или количества запусков
- Вычисления обычно представлены в виде функций
- Вычисления сохраняют знания в виде вычислений
Почему они лучше действий #
- Вычисления гораздо легче тестировать
- Гораздо легче поддаются статическому анализу
- Вычисления объединяются в вычисления
Важная особенность ФП — вынесение логики в вычисления из действий
Каких вопросов позволяют вычисления избегать? #
Вычисления гораздо легче воспринимать, так как можно не задаваться следующими вопросами:
- Какие операции ещё выполняет система в это время?
- Какие операции выполнялись ранее? Какие будут выполнены после?
- Сколько раз до этого это вычисление выполнялось?
Недостатки вычислений #
Невозможно узнать результат вычисления без запуска (как и у действия)
Конечно можно прочитать исходный код, но это не гарантирует 100% понимания. С точки зрения программы м только можем запустить функцию
Применение функционального мышления к существующему коду #
Рассмотрим существующий код по отправке комиссионных. sendPayout()
является действием, которое переводит деньги на банковский счёт
function figurePayout(affiliate) {
var owed = affiliate.sales * affiliate.commission;
if(owed > 100) {
sendPayout(affiliate.bank_code, owed);
}
}
function affiliatePayout(affiliates) { // Начальная функция
for(var i = 0; i < affiliates.length; i++) {
fiurePayout(affiliates[i]);
}
}
Определение типа кода #
№1 Изначальное знание о дейтсвиях #
function figurePayout(affiliate) {
var owed = affiliate.sales * affiliate.commission;
if(owed > 100) {
sendPayout(affiliate.bank_code, owed); // Действие
}
}
function affiliatePayout(affiliates) {
for(var i = 0; i < affiliates.length; i++) {
fiurePayout(affiliates[i]);
}
}
function main(affiliates) {
affiliatePayout(affiliates);
}
Определение типа кода #
№2 Определение типа figurePayout #
function figurePayout(affiliate) { // Действие
var owed = affiliate.sales * affiliate.commission;
if(owed > 100) {
sendPayout(affiliate.bank_code, owed); // Действие
}
}
function affiliatePayout(affiliates) {
for(var i = 0; i < affiliates.length; i++) {
fiurePayout(affiliates[i]); // Действие
}
}
function main(affiliates) {
affiliatePayout(affiliates);
}
Определение типа кода #
№3 Определение типа affiliatePayout #
function figurePayout(affiliate) { // Действие
var owed = affiliate.sales * affiliate.commission;
if(owed > 100) {
sendPayout(affiliate.bank_code, owed); // Действие
}
}
function affiliatePayout(affiliates) { // Действие
for(var i = 0; i < affiliates.length; i++) {
fiurePayout(affiliates[i]); // Действие
}
}
function main(affiliates) {
affiliatePayout(affiliates); // Действие
}
Определение типа кода #
№4 Определение типа main #
function figurePayout(affiliate) { // Действие
var owed = affiliate.sales * affiliate.commission;
if(owed > 100) {
sendPayout(affiliate.bank_code, owed); // Действие
}
}
function affiliatePayout(affiliates) { // Действие
for(var i = 0; i < affiliates.length; i++) {
fiurePayout(affiliates[i]); // Действие
}
}
function main(affiliates) { // Действие
affiliatePayout(affiliates); // Действие
}
Действия распространяются в коде #
Из примера видно, что действия делают код трудным: они распространяются в исходном коде
Если в функции применяется действие, то она тоже становится действием
Как правильно применять действия? #
Программисты на ФЯП используют действия (без этого приложения были бы бесполезны)
Применение действий происходит осознанно, к ним уделяется дополнительное внимание
Типичные виды действий #
Действия могут принимать различные формы
В большинстве языков программирования действия не отделены от вычислений
Рассмотрим примеры из языка JavaScript
- Вызов функций:
alert("Привет!");
- Вызов метода:
console.log("Был тут");
- Конструкторы:
new Date();
- Чтение глобальной (общей) переменной:
y
- Чтение глобального (общему) поля:
user.first_name
- Доступ к общим полям объекта:
stack[0]
- Изменение глобальной (общей) переменной:
z = 4
- Удаление поля общего объекта:
delete user.first_name
Любой код может стать действием, если начинает зависеть от количества обращений и времени обращения к ним
Детальное рассмотрение действий #
Действие — любой шаг, который может изменить состояние всей системы или зависит от него
В языке JavaScript действия реализуют в виде функций
Смысл действий заключается в изменении мира (состояния всей системы)
Другие названия действий #
- Нечистые функции
- Функции с побочными эффектами
Сложности работы с действиями #
- С действиями сложно работать
- Выполнение действий — основная задача программ
Подходы для работы с действиями в ФЯП #
- Необходимо использовать как можно меньше действий в приложении. По возможности необходимо заменять действия вычислениями
- Делать действия как можно маленькими. Из действий всю логику следует выделять в вычисления
- Ограничьте действия взаимодействием с внешним миром. Все остальные части программы желательно оформить вычислением и данными
- Уменьшите влияние фактора времени на действия
Заключение #
- Программисты на ФЯП выделяют три типа кода: действия, вычисления и данные.
- Действия зависят от времени вызова или от количества их вызовов. Обычно они содержат шаги, воздействующие на мир или которые зависят от мира
- Вычисления преобразуют входные данные в выходные. На их работу не влияет состояние вне них, не зависят от количества вызовов
- Данные описывают факты о событиях. Факты не изменяются со временем
- Программисты на ФЯП предпочитают данные вычислениям, а вычисления действиям
- Вычисления гораздо легче тестировать по сравнению с действиями, так как они всегда возвращают одинаковый результат на заданный вход