Функции как объекты первого рода. Часть № 2 #

Васильев Андрей Михайлович, 2022

Версии презентации

Содержание #

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

Один маркер в исходном коде и два рефакторинга #

Вспомним ключевые моменты предыдущего занятия

Маркер кода: неявный аргумент в названии фукнции #

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

  • Существует несколько функций с похожей реализацией
  • Разница между этими функциями описывается в названии функции

Рефакторинг: выделение неявного аргумента #

Цель данного рефакторинга — избавление от неявного аргумента. Это может помочь в выражении назначения кода и потенциально уменьшить дублирование

  1. Идентифицировать неявный аргумент в названии функции
  2. Добавить явный аргумент
  3. Использовать новый аргумент в теле функции вместо жёстко записанного значения
  4. Изменить вызывающий код

Рефакторинг: замена тела функцией обратного вызова #

Данный рефакторинг позволяет выделить «тело функции», часть функции, которая различается между функциями. Она заменяется функцией, которая передаётся в качестве аргумента

  1. Определить части функции: общее начало, тело, общее окончание
  2. Выделение всего кода под вопросом в функцию
  3. Выделение тела функции в функцию, которая передаётся в качестве аргумента

Рефакторинг копирования при записи #

При реализации шаблона копирование при записи мы часто дублировали код по созданию копий массивов и объектов. Шаблон копирования при записи:

  1. Создать копию
  2. Модифицировать копию объекта
  3. Вернуть модифицированную копию

К этому шаблону легко применить рефакторинг по замену тела функцией обратного вызова: пункт «модифцировать копию объекта» является телом

Реализация КПЗ для массивов #

Шаг № 1: определение частей функции

function arraySet(array, index, value) {
  var copy = array.slice();
  copy[index] = value;
  return copy;
}

function push(array, value) {
  var copy = array.slice();
  copy.push(value);
  return copy;
}

function drop_last(array) {
  var array_copy = array.slice();
  array_copy.pop();
  return array_copy;
}

function drop_first(array) {
  var array_copy = array.slice();
  array_copy.shift();
  return array_copy;
}

// Общая часть
function action(...) {
  var array_copy = array.slice(); // Общее начало
  // Тело
  return array_copy; // Общее окончание
}

Реализация КПЗ для массивов #

Шаг № 2: выделение тела в функцию

// Оригинал
function arraySet(array, index, value) {
  var copy = array.slice();
  copy[index] = value;
  return copy;
}

// Техническое выполнение шага
function arraySet(array, index, value) {
  return withArrayCopy(array);
}

function withArrayCopy(array) {
  var copy = array.slice();
  copy[index] = value; // Не будет работать
  return copy;
}

Реализация КПЗ для массивов #

Шаг № 3: выделение функции для обратного вызова

// Результат шага № 2
function arraySet(array, index, value) {
  return withArrayCopy(array);
}

function withArrayCopy(array) {
  var copy = array.slice();
  copy[index] = value;
  return copy;
}

// Выделение функции для обратного вызова
function arraySet(array, index, value) {
  return withArrayCopy(
    array,
    function(copy) {
      copy[idx] = value;
    });
}

function withArrayCopy(array, modify) {
  var copy = array.slice();
  modify(copy);
  return copy;
}

Реализация КПЗ для массивов #

Сравним оригинал функции и модификации

// Оригинал
function arraySet(array, index, value) {
  var copy = array.slice();
  copy[index] = value;
  return copy;
}

// Модификация
function arraySet(array, index, value) {
  return withArrayCopy(array, function(copy) {
    copy[index] = value;
  });
}

function withArrayCopy(array, modify) {
  var copy = array.slice();
  modify(copy);
  return copy;
}

Результат рефакторинга #

  • В результате рефакторинга количество строк кода увеличилось
  • Однако нам удалось реализовать стандартную КПЗ процедуру для массивов, которую не надо повторять во многих местах

Реализация быстрой сортировки с помощью специальной версии

var sortedArray = withArrayCopy(array, function(copy) {
  SuperSorter.sort(copy)
});

Можно выполнять несколько действий в рамках модификации

var a1 = drop_first(array);
var a2 = push(a1, 10);
var a3 = push(a2, 11);
var a4 = arraySet(a3, 0, 42);

var a4 = withArrayCopy(array, function(copy) {
  copy.shift();
  copy.push(10);
  copy.push(11);
  copy[0] = 42;
});

Практика #

Реализуйте с помощью реализованного метода withArrayCopy модифицируйте ранее созданные КПЗ-методы push, drop_last, drop_first

Решение

function push(array, element) {
  return withArrayCopy(array, function(copy) {
    copy.push(element);
  });
}

function drop_last(array) {
  return withArrayCopy(array, function(copy) {
    copy.pop();
  });
}

function drop_first(array) {
  return withArrayCopy(array, function(copy) {
    copy.shift();
  });
}

Практика. Реализация КПЗ для объектов #

Реализуйте метод withObjectCopy(), который может быть использован для реализации КПЗ для объектов. В качестве основы для создания данного метода используйте следующие функции:

function objectSet(object, key, value) {
  var copy = Object.assign({}, object);
  copy[key] = value;
  return copy;
}

function objectDelete(object, key) {
  var copy = Object.assign({}, object) ;
  delete copy[key];
  return copy;
}

Вариант решения задачи

function withObjectCopy(object, modify) {
  var copy = Object.assign({}, object);
  modify(copy);
  return copy;
};

function objectSet(object, key, value) {
  return withObjectCopy(object, function(copy) {
    copy[key] = value;
  });
}

function objectDelete(object, key) {
  return withObjectCopy(object, function(copy) {
    delete copy[key];
  });
}

Практика #

При реализации функции withLogging() мы смогли обобщить подход к выполнению журналирования произвольных ошибок во внешнюю систему

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

tryCatch(sendEmail, logToSnapErrors)
// вместо
try {
  sendEmail();
} catch(error) {
  logToSnapErrors(error);
}

Задача состоит в написании функции tryCatch()

Подсказка: данный метод должен принимать две функции в качестве аргументов

Вариант решения:

function tryCatch(f, errorHandler) {
  try {
    return f();
  } catch(error) {
    return errorHandler(error);
  }
}

Практика #

В качестве упражнения выполним оборачивание ещё одного элемента синтаксиса: условного оператора. Для упрощения задачи рассмотрим условный оператор без else

Пример кода, который необходимо модифицировать:

// Оригинал
if(array.length === 0) {
  console.log("Array is empty");
}
// Замена
when(array.length === 0, function() {
  console.log("Array is empty");
});

// Оригинал
if(hasItem(cart, "shoes")) {
  return setPriceByName(cart, "shoes", 0);
}
// Замена
when(hasItem(cart, "shoes"), function() {
  return setPriceByName(cart, "shoes", 0);
});

Вариант решения

function when(test, then) {
  if(test)
    return then();
}

Практикум #

После реализации функции when люди стали её использовать и захотели полноправный аналог if с else-выражением. Ваша задача — реализация функции IF:

IF(array.length === 0, function() {
  console.log("Array is empty");
}, function() {
  console.log("Array has something in it.");
});

IF(hasItem(cart, "shoes"), function() {
  return setPriceByName(cart, "shoes", 0);
}, function() {
  return cart;
});

Вариант решения

function IF(test, then, ELSE) {
  if(test)
    return then();
  else
    return ELSE();
}

Возвращение функций из функций #

При реализации метода withLogging() мы смогли избавиться от большой части дублирования, однако для её применения всё-равно приходится изменять много кода

try {
  saveUserData(user);
} catch(error) {
  logToSnapErrors(error);
}
withLogging(function() {
  saveUserData(user);
});

Мы экономим 2 строчки кода, однако всё ещё надо писать по 3 дополнительные строчки

function withLogging(f) {
  try {
    f();
  } catch (error) {
    logToSnapErrors(error);
  }
}

Дублирование с withLogging #

withLogging(function() {
  saveUserData(user);
});
withLogging(function() {
  fetchProduct(productID);
});

Данная система формирует стандартный подход к решению проблемы, но он имеет следующие проблемы:

  1. Можно забыть вызвать данный метод
  2. Данный код всё-равно необходимо вызывать везде

В примерах видно, что проблема дублирования существует

Было бы хорошо просто вызывать функции и автоматически получить желаемое поведение

Оригинальный код #

try {
  saveUserData(user);
} catch (error) {
  logToSnapErrors(error);
}
try {
  fetchProduct(productId);
} catch (error) {
  logSnapErrors(error);
}

Сделаем код более чётким, изменив название функций, чтобы они детальнее отражали отсутствие журналирования

Понятные имена #

try {
  saveUserData(user);
} catch (error) {
  logToSnapErrors(error);
}
try {
  fetchProduct(productId);
} catch (error) {
  logSnapErrors(error);
}
try {
  saveUserDataNoLogging(user);
} catch (error) {
  logToSnapErrors(error);
}
try {
  fetchProductNoLogging(productId);
} catch (error) {
  logSnapErrors(error);
}

Функции с журналированием #

try {
  saveUserDataNoLogging(user);
} catch (error) {
  logToSnapErrors(error);
}
try {
  fetchProductNoLogging(productId);
} catch (error) {
  logSnapErrors(error);
}
function saveUserDataWithLogging(user) {
  try {
    saveUserDataNoLogging(user);
  } catch (error) {
    logToSnapErrors(error);
  }
}
function fetchProductWithLogging(productId) {
  try {
    fetchProductNoLogging(productId);
  } catch (error) {
    logSnapErrors(error);
  }
}

Теперь мы чётко оделили функции с журналированием от оригинальных функций, но при вызове новых функций их точно не надо оборачивать

Потенциальное обобщение #

function (arg) {
  try {
    saveUserDataNoLogging(arg);
  } catch (error) {
    logToSnapErrors(error);
  }
}
function (arg) {
  try {
    fetchProductNoLogging(arg);
  } catch (error) {
    logSnapErrors(error);
  }
}

Применим первые шаги рефакторинга по замене тела с функцией обратного вызова

Определим общие и частные части

function (arg) {     // Начало
  try {              //
    saveUserDataNoLogging(arg);
  } catch (error) {  // Окончание
    logToSnapErrors(error);   //
  }                  //
}                    //
function (arg) {     // Начало
  try {              //
    fetchProductNoLogging(arg);
  } catch (error) {  // Окончание
    logSnapErrors(error); //
  }                  //
}                    //

Функция высшего порядка #

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

function (arg) {

  try {
    saveUserDataNoLogging(arg);
  } catch (error) {
    logToSnapErrors(error);
  }
}
function wrapLogging(f) {
  return function(arg) {
    try {
      f(arg);
    } catch (error) {
      logSnapErrors(error);
    }
  }
}

var saveUserDataWithLogging =
    wrapLogging(saveUserDataNoLogging);

Теперь можно легко создавать функции-обёртки:

var saveUserDataWithLogging = wrapLogging(saveUserDataNoLogging);
var fetchProductWithLogging = wrapLogging(fetchProductNoLogging);

Использование функции высшего порядка #

Ручной подход

try {
  saveUserData(user);
} catch (error) {
  logToSnapErrors(error);
}

Автоматический подход

saveUserDataWithLogging(user);

Последнее стало возможно благодаря работе функции wrapLogging:

var saveUserDataWithLogging = wrapLogging(saveUserData);
  • saveUserData — оригинальная функция
  • wrapLogging — функция высшего порядка, которая создаёт функцию-обёртку и возвращает её
  • saveUserDataWithLogging — функция-обёртка, созданная функцией высшего порядка

Обсуждение #

Результат работы функции withLogging сохраняется в переменную. Ранее в коде использовались функции, определённые с помощью слова function #

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

Надо быть готовым к расширенным подходам определения функций

Функция withLogging может делать обёртку только для функций с одним аргументом. А как её можно применить для функций с большим количеством аргументов? Как получить возвращаемое значение? #

Для возвращения значения — достаточно добавить в соответствующее место ключевое слово return к вызову функции

Для работы с произвольным количеством аргументов мы можем обратиться к возможностям языка, описанным в стандарте ES6 (2015 год):

function wrapLogging(f) {
  return function(...args) {
    try {
      return f(...args);
    } catch (error) {
      logToSnapErrors(error);
    }
  }
}

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

Практика #

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

Подсказка. Для решения этой задачи без функций можно использовать код:

try {
  codeThatMightThrow();
} catch(e) {
  // Игнорируем ошибку
}

Вариант решения

function wrapIgnoreErrors(f) {
  return function(...args) {
    return f(...args);
  } catch (error) {
    return null;
  }
}

Практика #

Реализуйте функцию высшего порядка makeAdder которая создаёт функцию, которая добавляет число к переданному аргументу. Пример работы:

var increment = makeAdder(1);
increment(10); // 11
var plus10 = makeAdder(10);
plus10(12); // 22

Вариант решения

function makeAdder(number) {
  return function(x) {
    return number + x;
  };
}

Обсуждение #

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

Если подойти технически к данному вопросу, то ответ скорее всего: да. Более правильный вопрос: стоит ли так поступать?

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

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

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

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

Заключение #

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

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