Функции как объекты первого рода. Часть № 1 #
Васильев Андрей Михайлович, 2022
Версии презентации
Содержание второй части курса #
В рамках первой части мы ознакомились с тем как разделять вычисления на три категории: действия, вычисления и данные. Познакомились со стратифцированной архитектурой.
В рамках второй части мы познакомимся с идеями объектов первого рода, в частности функций как объектов первого рода. На основании этого навыка мы познакомимся:
- С функциональными подходами к итерированию
- С формированием сложных вычислений путём составления цепочек из операций
- С новыми подходами по работе с глубоко вложенными данными
- Со способами контролирования последовательности выполнения действий и их повторений, чтобы решить проблемы синхронизации
- С новыми архитектурными подходами к организации служб
Содержание #
- Применение функций как объектов первого рода в исходном коде приложения
- Реализация синтаксиса языка с использованием функций как объектов первого рода
- Использование функций высшего порядка для обёртки любого синтаксиса
- Два рефакторинга, обеспечивающие использование функций как объектов первого рода и функций высшего порядка
Проблема дублирования кода #
В рамках данной главы мы будем в основном бороться с дублированием и будем искать более качественные абстракции. Сделаем краткий обзор
Маркер плохого кода: неявный аргумент в имени функции #
- Данный маркер указывает, что данный аспект кода может быть лучше выражен с помощью объектов первого рода
- Если мы используем значение в теле функции, и это значение присутствует в названии функции, это тоже является маркером
Характеристики #
- Очень похожие реализации функций
- Различие между функциями описывается в названии
Для решения проблем рекомендуется использовать следующие рефакторинги
Рефакторинг: выделение явного аргумента #
При наличии неявного аргумента сначала его необходимо сделать явным: добавить новый аргумент для функции, чтобы он стал объектом первого рода
Шаги:
- Идентифицировать неявный аргумент в названии функции
- Добавить явный аргумент
- Использовать новый аргумент вместо жёстко указанной зависимости
- Исправить код, вызывающий данную функцию
Рефакторинг: замена тела с помощью обратного вызова #
Синтаксис языка часто не является объектом первого рода. Данный рефакторинг позволяет заменить тело функции (часть, которая изменяется) на обратный вызов
Поведение функции будет определяться другой функцией, которая будет передана в качестве аргумента. Это мощный способ для создания функций высшего порядка
Шаги:
- Разделить код функции на секции: общий заголовок, тело, общее окончание
- Выделить общую функцию (заголовок и окончание), принимающую функцию в качестве аргумента
- Реорганизовать оригинальную функцию на вызов новой функции с передачей ей нужной функции в качестве аргумента
Отделу маркетинга всё ещё надо взаимодействовать с отделом разработки #
Реализованный ранее барьер из абстракций выполнял свои задачи, но не так хорошо, как предполагалось. Иногда команда маркетинга просит реализовать новые функции в API
Примеры новых запросов:
- Реализуйте метод для установки цены для товара корзины
- Реализуйте способ для установки значения количества для товара корзины
- Реализуйте способ установки времени доставки товара в корзине
Все запросы выглядят похожими, да и код для их реализации тоже выглядит похожим. Почему барьер из абстракций не помог решить данную проблему?
Ранее отдел маркетинга мог просто работать со структурами данных, а теперь им приходится ждать пока отдел разработки реализует необходимые функции. Это тормозит работу
Маркер плохого кода: неявный аргумент в названии функции #
Отделу маркетинга необходимо изменять товары в корзине, чтобы реализовать планы по их продвижению. Им необходимо делать доставку бесплатной для товара или делать его стоимость равной нулю
Рассмотрим функции, которые были реализованы для решения этих задач
function setPriceByName(cart, name, price) {
var item = cart[name];
var newItem = objectSet(item, 'price', price);
var newCart = objectSet(cart, name, newItem);
return newCart;
}
function setQuantitiyByName(cart, name, quantity) {
var item = cart[name];
var newItem = objectSet(item, 'quantity', quantity);
var newCart = objectSet(cart, name, newItem);
return newCart;
}
function setShippingByName(cart, name, shipping) {
var item = cart[name];
var newItem = objectSet(item, 'shipping', shipping);
var newCart = objectSet(cart, name, newItem);
return newCart;
}
function setTaxByName(cart, name, tax) {
var item = cart[name];
var newItem = objectSet(item, 'tax', tax);
var newCart = objectSet(cart, name, newItem);
return newCart;
}
- По структуре методы очень сильно совпадают друг с другом, они почти идентичны
- Данные методы отличаются только лишь названием свойства, которое они изменяют у товара в корзине
- Название свойства товара отражено в названии метода
- У данных функций присутствует неявный аргумент в названии
Маркеры в коде #
У неявного аргумента в названии метода есть две характеристики:
- Очень похожие реализации функций
- Название функции определяет различие в её реализации
Рефакторинг: выделение явного аргумента #
Если мы определили, что у функций присутствует неявный аргумент в названии, то мы можем преобразовать его в явный
Шаги рефакторинга:
- Идентифицировать неявный аргумент в названии функции
- Добавить явный аргумент
- Использовать новый аргумент вместо жёстко указанной зависимости
- Исправить код, вызывающий данную функцию
Реализуем функцию setFieldByName
, которая позволит изменять любое поле товара
function setPriceByName(cart, name, price) {
var item = cart[name];
var newItem = objectSet(item, 'price', price);
var newCart = objectSet(cart, name, newItem);
return newCart;
}
function setFieldByName(cart, name, field, value) {
var item = cart[name];
var newItem = objectSet(item, field, value);
var newCart = objectSet(cart, name, newItem);
return newCart;
}
cart = setPriceByName(cart, "shoe", 13);
cart = setQuantityByName(cart, "shoe", 3);
cart = setShippingByName(cart, "shoe", 0);
cart = setTaxByName(cart, "shoe", 2.34);
cart = setFieldByName(cart, "shoe", "price", 13);
cart = setFieldByName(cart, "shoe", "quantity", 3);
cart = setFieldByName(cart, "shoe", "shipping", 0);
cart = setFieldByName(cart, "shoe", "tax", 2.34);
- Данный рефакторинг в рамках текущего кода позволил заменить четыре существующие функции с помощью одной более общей
- Название поля товара стало объектом первого рода
- Ранее название поля товара было представлено в API неявным образом как часть названий функций
- Сейчас название поля можно передать в качестве аргумента, записать массив и так далее, это и делает её объектом первого рода. Мы можем использовать все возможности языка для работы с ними
Строки для обозначения полей #
Мы получили очень гибкий инструмент, возможно даже слишком гибкий, так как для обозначения поля мы используем строки
Общение между отделом маркетинга (М) и разработчиками (Р)
- М: нам теперь не нужно писать сообщения по поводу каждого поля?
- Р: да, теперь вы можете работать с любым полем, каким захотите.
- М: как мы узнаем названия полей?
- Р: это легко, мы сделаем их частью спецификации на API!
- M: звучит хорошо. А если вы добавите новые поля к корзине или товарам, что дальше?
- Р: данная функция позволит работать и с этим полем тоже. При добавлении нового поля, мы опубликуем его название, после этого можно его использовать
- М: такой подход выглядит более удобным для нас.
- Р: вместо набора функций вам достаточно знать одну функцию и набор вариантов названий полей.
Что должно и что не должно быть объектом первого рода #
В языке JavaScript и во многих других языках программирования многие функции не являются объектами первого рода
Посмотрим на действия, которые мы можем сделать с числом: передать в функцию, вернуть из функции, сохранить в переменную, поместить как значение в массиве или ассоциативном массиве. Аналогично для других структур данных
Мы не можем сделать это для (например):
- операций сложения, вычитания, умножения и т.д.
- ключевого слова
if
, ключевого словаfor
Они не являются объектами первого рода
Рассмотрим проведённый рефакторинг #
Мы создали в прошлый раз следующее изменение:
function setPriceByName(cart, name, price)
function setFieldByName(cart, name, field, value)
Мы смогли взять элемент языка, название функции, которое не является объектом первого рода, и заменили её на аргумент функции, строку, которая является объектом первого рода. Это позволило избавиться от дублирования
Данный подход к преобразованию элементов языка в объекты первого рода будет использован во второй части курса
Приведёт ли использование строк к увеличению числа ошибок? #
Сейчас имя поля передаётся в качестве строки. При наборе данной строки легко совершить ошибку
Проверка кода может осуществляться:
- Во время компиляции (или статического анализа)
- Во время исполнения
Если бы язык программирования поддерживал концепцию перечисления, то мы могли бы заменить строку на элемент перечисления. Тогда проверку можно было бы провести в фазе статического анализа
В рамках языка JavaScript, в котором нет перечисления, мы могли бы реализовать проверку во время выполнения
var validItemFields = ['price', 'quantity', 'shipping',
'tax'];
function setFieldByName(cart, name, field, value) {
if(!validItemFields.includes(field))
throw "Not a vaild item field: '" + field "'.";
var item = cart[name];
var newItem = objectSet(item, field, value);
var newCart = objectSet(cart, name, newItem);
return newCart;
}
Будет ли сложно изменять API? #
Мы в барьер из абстракций переносим имена полей объекта товар. Ранее они находились за барьером и пользователи их не видели. Не ломаем ли мы барьер из абстракций?
- Помещая что-то в API, мы гарантируем, что оно не будет изменяться
- Однако мы можем оставить API неизменным, изменив реализацию внутри
Рассмотрим ситуацию, когда мы хотим изменить поле quantity
на number
. В этом случае мы можем реализовать замену на уровне реализации
var validItemFields = ['price', 'quantity', 'shipping',
'tax'];
var translations = { 'quantity': 'number' }
function setFieldByName(cart, name, field, value) {
if(!validItemFields.includes(field))
throw "Not a vaild item field: '" + field "'.";
if(translations.hasOwnProperty(field))
field = translations[field];
var item = cart[name];
var newItem = objectSet(item, field, value);
var newCart = objectSet(cart, name, newItem);
return newCart;
}
Практикум #
Кто-то в команде написал следующий набор функций, имеющих неявный аргумент в именах функций. Примените к ним рефакторинг по выделению явного аргумента, чтобы убрать дублирование
function multiplyByFour(x) {
return x * 4;
}
function multiplyBySix(x) {
return x * 6;
}
function multiplyBy12(x) {
return x * 12;
}
function multiplyByPi(x) {
return x * 3.14159;
}
Ответ:
function multiply(x, y) {
return x * y;
}
Практикум #
В рамках разработки пользовательского интерфейса были реализованы две функции-обработчика нажатия на кнопки увеличения количества и увеличения размера одежды. Примените к данному коду такой же рефакторинг:
function incrementQuantityByName(cart, name) {
var item = cart[item];
var quantity = item['quantity'];
var newQuantity = quantity + 1;
var newItem = objectSet(item, 'quantity', newQuantity);
var newCart = objectSet(cart, name, newItem);
return newCart;
}
function increementSizeByName(cart, name) {
var item = cart[name];
var size = item['size'];
var newSize = size + 1;
var newItem = objectSet(item, 'size', newSize);
var newCart = objectSet(cart, name, newItem);
return newCart;
}
Ответ:
function incrementFieldByName(cart, name, field) {
var item = cart[item];
var value = item[field];
var newValue = value + 1;
var newItem = objectSet(item, field, newValue);
var newCart = objectSet(cart, name, newItem);
return newCart;
}
Практикум #
Команда разработки озабочена тем, что люди начинают использовать последний метод для увеличения значения других полей. Однако это не разумно для полей названия или цены.
Реализуйте защиту от попыток изменения других полей элемента корзины
function incrementFieldByName(cart, name, field) {
if(field !== 'size' && field !== 'quantity') {
throw "This item field can not be incremented " +
field + "'.";
}
var item = cart[item];
var value = item[field];
var newValue = value + 1;
var newItem = objectSet(item, field, newValue);
var newCart = objectSet(cart, name, newItem);
return newCart;
}
Использование массивов и ассоциативных массивов #
Мы добились возможности описания свойств с помощью объектов первого рода
- В языке JavaScript для представления ассоциативных массивов используются объекты
- В языке Haskell возможным решением будет использование алгебраических типов
- В языке Java можно воспользоваться HashMap
- В Ruby можно указывать имена методов с помощью символов
В любом случае мы стараемся обращаться с данными напрямую, без создания специфических обёрток. Созданные интерфейсы только обеспечивают интерпретацию этих данных в рамках данного сценария использования
Корзина и товар в корзине сами по себе описывают достаточно общие концепции и находятся «внизу» графа вызовов
На основании этих общих структур можно построить различные специфические интерфейсы и барьеры из абстракций
Статическая и динамическая типизация #
- Языки со статической типизацией проверяют типы переменных во время компиляции
- Языки с динамической типизацией выполняют проверку во время выполнения
Ведётся активное противостояние между сторонниками каждого из подходов к формированию особенностей языка, ни одна из сторон не одержала победу
Есть исследование, согласно которому на качество результирующего продукта сильнее влияет качество сна программиста
В рамках книги и курса используется динамически типизированный язык JavaScript ввиду своей популярности и использования распространённого Си-подобного синтаксиса
Выбор языка программирования для разработки конкретного приложения обусловлен множеством факторов. Главное, чтобы финальный выбор устраивал всех членов команды и они могли выспаться
Вопрос использование строк в качестве идентификаторов #
Во многих языках программирования используются строки (или подобные им сущности) для идентификации полей в рамках ассоциативных массивов (JavaScript, Ruby, Clojure, Python). Ошибки в данных строках могут приводить к ошибкам
Эмпирический факт: на таких языках разработаны критически-важные сервисы
В современных веб-приложениях для общения между клиентом и сервером используется JSON. Для общения приложения с базой данных SQL
То есть функционирование систем зависит от корректности формирования строковых запросов друг другу. Даже если бы они были бинарными, то это оставляет широкое пространство для появления ошибок
Даже в языках со статической типизацией необходимо проверять данные, приходящие извне: от пользователя, посредством HTTP-запроса и т.д.
Функции как объекты первого рода могут заменить любой синтаксис #
Ранее мы рассмотрели, что в языке JavaScript много элементов не являются сущностями первого рода
Например, мы не можем присвоить оператор сложения в переменную
Однако мы можем реализовать эквивалент данной операции на основе функции:
function plus(a, b) {
return a + b;
}
На первый взгляд это может показаться глупым, но давайте рассмотрим возможные использования для функции сложения, которую будем использовать как сущность первого рода
Практикум #
Реализуйте похожие функции для других математических операций: умножение, вычитание и деление
function times(a, b) {
return a * b;
}
function divideBy(a, b) {
return a / b;
}
function minus(a, b) {
return a - b;
}
Проблемы отдела маркетинга #
При разработке своих функций отдел маркетинга совершает множество ошибок, в частности при реализации обхода по списку товаров. Они хотят иметь возможность не использовать for-циклы
Для решения этой задачи мы можем воспользоваться подходом к оборачиванию данного действия функцией. Однако данная функция не будет использована как функция первого рода, она будет использована как функция высшего порядка
Функцией высшего порядка называют функцию, которая либо принимает другую функцию в качестве аргумента, либо возвращает новую функцию в качестве возвращаемого значения
Функции высшего порядка не могут быть реализованы, если язык не позволяет использовать функции как объекты первого рода
Работа с циклом for #
Рассмотрим два следующих метода, которые используют цикл for для решения типичных задач по приготовлению еды и мытья посуды
for(var i = 0; i < foods.length; i++) {
var food = foods[i];
cook(food);
eat(food);
}
for(var i = 0; i < dishes.length; i++) {
var dish = dishes[i];
wash(dish);
dry(dish);
putAvay(dish);
}
- Код решает различные задачи, однако по форме функции очень похожи
- Проведём ряд изменений кода, чтобы они стали ещё более похожи друг на друга
Оборачиваем в функции #
function cookAndExatFoods() {
for(var i = 0; i < foods.length; i++) {
var food = foods[i];
cook(food);
eat(food);
}
}
cookAndEatFoods();
function cleanDishes() {
for(var i = 0; i < dishes.length; i++) {
var dish = dishes[i];
wash(dish);
dry(dish);
putAvay(dish);
}
}
cleanDishes();
Обобщаем название переменных #
function cookAndExatFoods() {
for(var i = 0; i < foods.length; i++) {
var item = foods[i];
cook(item);
eat(item);
}
}
cookAndEatFoods();
function cleanDishes() {
for(var i = 0; i < dishes.length; i++) {
var item = dishes[i];
wash(item);
dry(item);
putAvay(item);
}
}
cleanDishes();
Добавляем аргумент #
function cookAndExatFoods(array) {
for(var i = 0; i < array.length; i++) {
var item = array[i];
cook(item);
eat(item);
}
}
cookAndEatFoods(foods);
function cleanDishes(array) {
for(var i = 0; i < array.length; i++) {
var item = arary[i];
wash(item);
dry(item);
putAvay(item);
}
}
cleanDishes(dishes);
Выделяем тело методов #
function cookAndExatFoods(array) {
for(var i = 0; i < array.length; i++) {
var item = array[i];
cookAndEat(item);
}
}
function cookAndEat(food) {
cook(food);
eat(food);
}
cookAndEatFoods(foods);
function cleanDishes(array) {
for(var i = 0; i < array.length; i++) {
var item = arary[i];
clean(item);
}
}
function clean(dish) {
wash(dish);
dry(dish);
putAway(dish);
}
cleanDishes(dishes);
Неявный аргумент в названии #
function operateOnArray(array, f) {
for(var i = 0; i < array.length; i++) {
var item = array[i];
f(item);
}
}
function cookAndEat(food) {
cook(food);
eat(food);
}
cookAndEatFoods(foods, cookAndEat);
function operateOnArray(array, f) {
for(var i = 0; i < array.length; i++) {
var item = arary[i];
f(item);
}
}
function clean(dish) {
wash(dish);
dry(dish);
putAway(dish);
}
cleanDishes(dishes, clean);
Избавляемся от дублирования #
function forEach(array, f) {
for(var i = 0; i < array.length; i++) {
var item = array[i];
f(item);
}
}
function cookAndEat(food) {
cook(food);
eat(food);
}
forEach(foods, cookAndEat);
function clean(dish) {
wash(dish);
dry(dish);
putAway(dish);
}
forEach(dishes, clean);
Сравнение начала и результата #
for(var i = 0; i < foods.length; i++) {
var food = foods[i];
cook(food);
eat(food);
}
for(var i = 0; i < dishes.length; i++) {
var dish = dishes[i];
wash(dish);
dry(dish);
putAway(dish);
}
forEach(foods, function (food) {
cook(food);
eat(food);
});
forEach(dishes, function (dish) {
wash(dish);
dry(dish);
putAway(dish);
});
- Для реализации примера используем анонимные функции
forEach
позволяет пройтись по всем элементам массива без использования индексов, больше не надо писать цикл forforEach
является функцией высшего порядка, так как она принимает другую функцию в качестве своего аргумента
Общий обзор процесса создания функции высшего порядка #
Рассмотрим общий процесс, который был проведён для создания функции высшего порядка на примере forEach
- Оборачивание целевого кода в функцию
- Переименование функции, чтобы сделать её общеприменимой
- Выделение неявного аргумента
- Выделение функции-тела
- Выделение явного аргумента-функции
Кажется, что шагов очень много, рассмотрим другой подход
Отлавливание ошибок #
В рамках решения задачи повышения надёжности работы приложения было принято решение о сохранении информации об ошибке во внешней системе
try {
saveUserData(user);
} catch (error) {
logToSnapErrors(error); // Нужный вызов
}
Основная проблема — необходимо добавлять стереотипный код во многие места
Стоит отметить, что добавление информации об ошибке в систему журналирования не является нормальной стратегией для обработки исключительных ситуаций. Однако ситуации, которые программно обработать невозможно, необходимо записывать в такие системы, чтобы их можно было решить
Рефакторинг: замена тела с помощью метода обратного вызова #
try {
saveUserData(user);
} catch (error) {
logToSnapErrors(error);
}
try {
fetchProduct(productId);
} catch (error) {
logToSnapErrors(error);
}
Порядок выполнения рефакторинга:
- Определить в коде общий заголовок, тело, общее окончание
- Выделить весь код в отдельную функцию
- Выделить определённое тело функции в отдельную функцию, которая будет передаваться в качестве аргумента
В языке JavaScript функция, которую передают в качестве аргумента, называется функцией обратного вызова, callback
Применение рефакторинга #
// Оригинал
try {
saveUserData(user);
} catch (error) {
logToSnapErrors(error);
}
// Шаг № 1: Выделение частей
try { // Общий заголовок
saveUserData(user); // Тело
} catch (error) { // Общее окончание
logToSnapErrors(error);
}
// Шаг № 2: Выделение кода в функцию
function withLogging() {
try {
saveUserData(user);
} catch (error) {
logToSnapErrors(error);
}
}
withLogging();
// Шаг № 3: Выделение функции
function withLogging(f) {
try {
f();
} catch(error) {
logToSnapErrors(error);
}
}
withLogging(function() {
saveUserData(user);
});
Определение функций в JavaScript #
Глобальное определение функций #
Глобально определённые функции могут быть использованы везде в приложении
function saveCurrentUserData() {
saveUserData(user);
}
withLogging(saveCurrentUserData);
var anotherSave = function() {
saveUserData(user);
};
withLogging(anotherSave);
Локально определённые функции #
Функции могут быть определены внутри другой функции. Это позволяет им взаимодействовать с другими переменными, определёнными внутри данной функции
function someFunction() {
var saveCurrentUserData = function() {
saveUserData(user);
};
withLogging(saveCurrentUserData);
}
Анонимные встроенные функции #
Функцию можно определить сразу в том месте, где она должна использоваться, она будет называться встроенной функции
Если функции после определения не было дано имя, то она является анонимной
withLogging(function() { saveUserData(user); });
Оборачивание кода функцией #
При оборачивании кода функцией он не будет вызван в месте декларирования функции, а в месте вызова данной функции
После определения функций с ними можно выполнять различные действия:
// Сохранять в переменные
var f = function() {
saveUserData(user);
};
// Сохранять в массиве
array.push(function() {
saveUserData(user);
});
// Передавать в другие функции
withLogging(function() {
saveUserData(user);
});
Функции высшего порядка самостоятельно определяют время вызова переданных им функций
// Не вызывать функцию
function callOnThursday(f) {
if(today === "Thursday")
f(); // Вызов только по четвергам
}
// Вызвать функцию позже
function callTomorrow(f) {
sleep(oneDay);
f();
}
// Вызвать функцию в новом контексте
function withLogging(f) {
try {
f();
} catch(error) {
logToSnapErrors(error);
}
}
Обсуждение #
Рассмотренный рефакторинг замены тела с помощью функции обратного вызова полезен для уменьшения дублирования в определённых ситуациях. Есть ли другие применения? #
В некотором плане да: он позволяет устранить дублирование
Аналогично с функциями высшего порядка: они позволяют выполнить запуск функции вместо дублирования тела метода
Почему мы передаём функцию? Почему не данные? #
Важный аспект: мы запускаем переданный код в нужном контексте
Предположим, что мы выполним следующий код
withLogging(saveUserData(user));
В этом случае метод withLogging не сможет выполнить свою функцию — обработать исключение
Заключение #
- Объектами первого рода называются всё, что можно сохранить в переменную, передать в виде аргумента и вернуть функцию. Объектами первого рода можно взаимодействовать на уровне кода
- Многие части языка не являются объектами первого рода. Их можно обернуть с помощью функций, чтобы сделать их объектами первого рода
- Некоторые языки поддерживают концепцию функций как объектов первого рода
- Функциями высшего порядка называют функции, которые используют другие функции как объекты первого рода. Функции высшего порядка позволяют делать абстракции над поведением
- Неявный аргумент в названии функции является признаком плохого кода. Можно использовать рефакторинг выделения неявного аргумента, чтобы сделать аргумент явным вместо недоступной части имени функции
- Можно применить рефакторинг замены тела метода с помощью функции обратного вызова, чтобы абстрагироваться от поведения. С его помощью мы можем сформировать аргумент в виде функции как аргумента первого рода, который показывает разницу в поведении между двумя функциями