Преди известно време в subreddit на Clojure беше зададен въпросът: „Някой има ли конкретни примери за това, когато променливостта го е вкарала в беда?“

Отговорът е да, имам някои.

Популярна библиотека за обработка на кредитни карти (нека я наречем Foo)

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

Но след това забелязах, че ако сте въвели лоша карта (изтекла, да речем), полето за дата на формуляра ми изведнъж ще стане празно. защо беше така

При проверка открих, че състоянието на кредитната ми карта се е променило от това:

{
  exp: '10/20',
}

До това:

{
  expMonth: '10',
  expYear: '20',
}

Какво?!? Търсих в кодовата база за expMonth и не можах да го намеря. Проверих два пъти манипулатора на събития onChange на моето exp поле за въвеждане. Проследих кода по всеки маршрут, за който се сетих, докато не ме отведе до повикването на Foo. Foo изтриваше свойство от моя обект и заменяше това свойство с две различни свойства, които кодът ми не очакваше или разбираше. Излишно е да казвам, че бях много раздразнен.

Дати в Испания

В ECMAScript датите са променливи. Това ме натъжава. Често се сблъсквам с бъгове, които са нещо подобно:

// Hey, I want to create a date range with a start and end!
const start = opts.startDate || new Date();
// The end date should default to the start date
const end = opts.endDate || start;
// Many lines of code later…
// I’m gonna shift my start date a bit…
start.setDate(start.getDate() — 1);

В един момент забелязвам, че стойността ми end не е това, което очаквах... Поставям точки на прекъсване или изрази за печат на всяко място, където end се променя. Никой от тях не бива ударен.

АРГХ!!! Хвърлям лаптопа си през прозореца и решавам да сменя кариерата.

На следващия ден, след добра нощна почивка, си спомням: „О, да. Фурмите са променливи в ES… Turds.“

Низове в C++

Обикновено, когато вашият модул разкрива публична функция, вие проверявате нейните аргументи, преди да продължите. Ето някои (непроверени и глупави) псевдокодове:

const speech = {
  cat: () => 'Meow',
  dog: () => 'Woof',
};
function pet(kind: string) {
  // Let’s validate that kind exists in our speech map
  assert(exists(speech[kind]));
  // We’ll return a function which will speak like our kind of pet!
  return () => speech[kind]();
}

Имайки предвид този код, може да се изненадате от следното поведение:

// Here, str = 'cat'
const mittensSays = pet(str);
mittensSays(); // 'Meow'
mittensSays(); // 'Meow'
// Do lots of stuff…
mittensSays(); // Uncaught exception!

Как е могло това да се случи? Проверихме, че kind е валиден и е заловен в затваряне, така че никой няма достъп до него! Обаждането на mittenSays ни дава 'Meow'. Как може по-късно да хвърли изключение?!?

Когато сте чели този код, вероятно сте предположили, че низовете са неизменни. Низовете вероятно са неизменни на избрания от вас език. Но навремето направих своя дял от програмирането на C++. (Никога не съм бил много добър в това.) Преди редовно мутирах низове. Така че, ако подадете низ към моята функция upperCase, аз директно бих преобразувал низа ви в главни букви чрез мутация, а не чрез създаване на копие.

Начинът, по който предишният пример се провали беше, че някой презаписа стойности в str така:

str[0] = 'B'; // Making `str = 'Bat'`

Подобни неща направиха моите C++ програми наистинатрудни за диагностициране на проблеми като този в примера mittenSays. И докато низовете обикновено не могат да се променят на други езици, можете да се забъркате в подобни бъркотии чрез мутиране на масиви и обекти.

Многонишкови главоболия

(Глупаво) писах свои собствени многонишкови C# сървъри. В наши дни оставям такива неща на супер хубави OSS продукти, написани от по-умни разработчици от мен. Но по-рано в кариерата си направих купища собствена многонишкова логика. Няма да навлизам в подробности, но е достатъчно да кажа, че мутацията + многонишковостта не е ваш приятел. Отстраняването на грешки при изключения, състезателни условия и блокирания е огромна болка. Повечето (но не всички) от моите многонишкови проблеми се дължаха на споделяне на променливо състояние между нишки. В крайна сметка научих урока си и се преместих от споделено променливо състояние към нещо подобно на актьорския модел. По принцип, колкото е възможно по-често, координирах нишки, като предавах неизменни съобщения между тях. Запазих споделеното променливо състояние за много малко, строго контролирани конструкции.

Компоненти за отстраняване на грешки

Някои от най-трудните бъгове в потребителския интерфейс, с които трябваше да се справя, идват от сценарии, при които обект е бил мутиран някъде, но не можах да разбера къде или защо. Това е подобно на примера за обработка на кредитна карта Foo. Преди беше обичайно да отстранявам грешки в UI компонент в моето приложение и да забелязвам, че вътрешното му състояние има странна, неочаквана стойност. След много ровене щях да открия, че част от вътрешното ми състояние всъщност беше споделена с друг неправилно работещ компонент. В моите стари Angular 1 приложения и моите стари C# приложения, този вид неща ме подлудиха... Clojure (Script) и/или React + redux, с акцент върху неизменността, до голяма степен оставиха тази болка зад мен.

Тестване на единици

Пиша тестове като следните през цялото време и след това трябва да се върна и да се коригирам:

test('appendTitle only modifies the titles property', () => {
  const prevState = {
    series: 'Lord of the Rings',
    titles: ['The Fellowship of the Ring'],
  };
  const nextState = appendTitle(prevState, 'The Two Towers');
  assert.equal(nextState, {
    series: prevState.series,
    titles: ['The Fellowship of the Ring', 'The Two Towers'],
  });
});

Тук се опитвам да тествам дали appendTitle добавя заглавие към масива titles, но оставя останалата част от обекта непроменена. Тестът изглежда сравнително лесен, нали? Но тестът преминава дори със следната реализация на appendTitle:

function appendTitle(movieSeries, title) {
  movieSeries.series = 'MWAHA HA HA HA I CHANGED IT, SUCKA!';
  movieSeires.titles.push(title);
  return movieSeries;
}

Опа!

Мисли за раздяла

Езиковите дизайнери (като цяло) осъзнаха, че низовете трябва да бъдат неизменни по подразбиране. Повечето езикови дизайнери също са осъзнали, че датите трябва да бъдат неизменни по подразбиране. Но някак си повечето езикови дизайнери не са направили тази логика една крачка напред и не са я приложили към всички данни. В най-популярните езици за програмиране обектите, картите, списъците и т.н. са променливи по подразбиране. Това обикновено означава много защитно копиране или много фини грешки, или и двете.

Когато пиша код, обикновено мисля само за тесен път през моята кодова база. Мисля за много специфичния проблем, който съм се заел да разреша. В миналото, когато мутирах данни, това обикновено беше при тези условия. Предположих, че моят код притежава модифицираните данни и че ще се използва само в този конкретен сценарий. Не обмислях възможността някой ден данните ми да бъдат споделени между различни компоненти / обекти / функции / каквото и да било. И така, въведох фини грешки.

Тези дни, когато наставлявам студенти, ги съветвам да запазят променливостта за два сценария:

  • оптимизации на производителността (рядко)
  • локално изграждане на данни (по-рядко)

Това, което имам предвид под „локално изграждане на данни“, е когато изграждате нова структура от данни, която е изцяло собственост на вашата функция (например, когато извършвате редуциране върху списък, натрупващият обект трябва бъдете изцяло местни). Тук все още предпочитам неизменността, но в зависимост от езика може да е по-ясно (и почти винаги по-производително) да се промени структурата на данните, която се изгражда. Тъй като данните са изцяло локални, мутациите няма да имат външни странични ефекти и са доста лесни за разсъждение поради локалността на кода. След като данните бъдат върнати/конструирани, считам, че те са неизменни.

С течение на времето личните ми навици се промениха - както и езиковите ми предпочитания. Днес, доколкото е възможно, се опитвам да избягвам писането на променлив код. Сега силно предпочитам езици, които са неизменни по подразбиране.