Про фундаментальні помилки в дизайні мов програмування

якось мені на очі потрапила стаття про те, що найдорожчою помилкою в дизайні мов програмування було рішення визначати закінчення рядка в C за NULL-байту. Один з варіантів перекладу цієї статті на Хабре (хоча я, по-моєму, читав інший). Ця стаття мене трохи здивувала. По-перше, як-ніби в ті часи економії кожного біта пам'яті можна було шиканути і виділити ще 2-4 байта в кожному рядку на зберігання її розміру. По-друге, ніяких особливо катастрофічних наслідків це рішення для програміста не несе. Помилок, які можна з цього приводу здійснити я можу придумати цілих дві: невірно виділити пам'ять для рядка (забути місце під NULL) і невірно записати рядок (забути NULL). Про першу помилку вже попереджають компілятори, уникнути другої допомагає використання бібліотечних функцій. Всієї біди.

Значно більшою проблемою часів дизайну мови (і потім З++) мені здається інше — оператор for. При всій його очевидній нешкідливості — це просто джерело потенційних помилок і проблем.

Згадаймо класичне його застосування:

for (int i = 0; i < vec.size(); i++)
{...}
Що ж тут може піти не так?

1.
for (<b>int</b> i = 0; i < vec.size(); i++)


Незважаючи на те, що приклад з int найчастіше йде в підручниках на перших сторінках, використання int частіше всього неправильно. Ми, в основному, проходимся по масивам\векторах\списками. Тобто по-перше нам потрібен беззнаковий тип, а по-друге, нам потрібний тип даних, який відповідає максимальному розміру використовуваної колекції. Тобто правильно було б написати

std::vector < int>::size_type

Скажіть, чи часто ви таке писали? Ось саме. Це виглядає настільки страшно, що мало в кого вистачало сили волі скрізь писати подібне. У результаті ми маємо мільйони неправильно написаних циклів. Що це, якщо не помилка в дизайні мови програмування?

2.
for (int <b>i</b> = 0; i < vec.size(); i++)

Всіх програмістів навчають грамотно іменувати змінні. За імена на кшталт «a, b, temp, var, val, abra_kadabra» дають по руках викладачі на парах, ну або старші колеги молодим джуниорам. Однак є виняток. «Ну, якщо це лічильник в циклі, то можна просто i або j». Бр-р-р-р. Стоп! Тобто давати коректні імена змінних потрібно у всіх випадках… крім ось цих випадків, коли змінним з якихось причин зрозумілі імена не потрібні і можна написати одну незрозумілу букву? Це чому це так вийшло? А вийшло так тому, що якщо б змусити програміста назвати змінну «currentRowIndex», то в циклі for її довелося б написати тричі:

for (int currentRowIndex = 0; currentRowIndex < vec.size(); currentRowIndex++)

У результаті довжина рядка виростає з 37 до 79 символів, що незручно ні читати, ні писати. Так що ми пишемо i. Що призводить до того, що у внутрішньому циклі for ми вже використовуємо j, у якому-небудь алгоритмі Флойда — Уоршелла Вікіпедія рекомендує нам для третього рівня циклу використовувати змінну k і так далі. Крім очевидною неочевидності написаного коду, ми тут маємо ще й помилки копипасты. Візьміть напишіть який-небудь перемножування матриць, з першого разу не переплутавши ніде змінні i і j, кожна з яких в одному місці коду означає стовпчик, а в іншому — рядок матриці.

Ми живемо з цим з-за поганого дизайну циклу for.

3.
for (int i <b>= 0</b>; i < vec.size(); i++)

Біда з циклом for в тому, що як правило, нам потрібно починати його перегляд з нульового елемента. Крім тих випадків, коли потрібно з першого, другого, знайденого раніше, останнього, закешированного і т. д. Набита рука програміста звично копипастит пише = 0, а далі потрібно налагодження і згадування кузькину мать, щоб виправити таке звичне = 0 на потрібний варіант. Ви скажете, що вини for тут немає, а є неуважність програміста? Я не погоджуся. Якщо того ж програміста попросити написати той же код з допомогою do\while або while — він напише його з першого разу без помилки. Тому, що у нього перед очима в даному випадку не буде приївся шаблону, всі цикли do\while або while досить унікальні, програміст кожен раз думає, з чого починається цикл і за яким критерієм він зупиняється. У дизайні циклу for ця необхідність думати іноді здається зайвою, з-за чого нею пренебрагают практично завжди.

4.
for (<b>int i = 0</b>; i < vec.size(); i++)

Зручна особливість циклу for полягає в тому, що змінна i створюється в області видимості циклу і знищується при виході з неї. Це, загалом, добре і іноді дозволяє заощадити пам'ять або якось задіяти RAII. Але це зовсім не працює в тих випадках, коли нам потрібно щось знайти в циклі і зупинитися. Зупинитися-то ми можемо, але щоб повернути індекс знайденого елемента — нам потрібна додаткова змінна. Або визначення i до циклу. Зайва змінна — це невиправдані витрати для тих випадків, коли нічого знайдено не буде. Оголошення i до циклу ламає стрункість коду — перша секція for залишається порожньою, що змушує читача вдумуватися в код вище, намагаючись зрозуміти чи це помилка, чи то так і було треба.

Можливо, це виглядає причіпкою, але для мене циклу for бракує можливості повернути значення індексу у разі дострокової зупинки. Це могло б виглядати як який-небудь пост-блок (зразок else для циклу while), в якому було б доступно останнє значення лічильника ітерацій. Або функція в дусі GetLastError(), яка повертала б останнє значення змінної i на момент виклику break;

5.
for (int i = 0; <b>i < vec.size()</b>; i++)

Перевірка умови у другому блоці оператора for не виглядає логічною, оскільки на кожній ітерації циклу (крім першої) спочатку буде виконуватися інкремент лічильника (третій блок) потім перевірка умови (другий блок). Перевірка умови знаходиться у другому блоці, щоб підкреслити той факт, що вона буде виконуватися за першої ітерації циклу відразу після ініціалізації лічильника i — тільки при цьому поясненні все виглядає більш-менш логічно. В результаті ми отримали цикл, синтаксис якого сконцентрований на першій його ітерації і погано відображає те, що відбувається на всіх наступних (яких зазвичай в рази більше). Такий вже дизайн оператора for.

6.
for (int i = 0; i <b>< vec.size()</b>; i++)

«Менше». Або «менше»? Або «не одно»? До ".size()" або ".size() — 1"? Так, на ці питання легко знайти відповідь, але чому, скажіть, ці питання взагалі можна\потрібно задавати собі? І як у тих рідкісних випадках, коли потрібно написати нестандартний варіант дати знати колегам-програмістам, що це не помилка, а саме так ти і збирався написати?

7.
for (int i = 0; i < <b>vec</b>.size(); i++)

Це взагалі єдине місце, де ми розповідаємо циклу, за якою, власне, колекції збираємося ходити. Та й то, згадуємо ми її лише в контексті розміру. Ось, мовляв, стільки-то кроків потрібно зробити. При цьому в самому циклі ми цілком можемо ходити по вектору vec2, який, звичайно ж, за законом підлості, в дебаге буде мати таку ж довжину, а в релізі обов'язково іншу, з-за чого ми виявимо цей баг значно пізніше того моменту, коли потрібно було це зробити.

7.
for (int i = 0; i < vec<b>.size()</b>; i++)

Як люди тільки не придумують позначення кількості елементів колекції! Так, STL зі своїм size() досить консистентен, але інші бібліотеки используеют і length(), count(), number() і totalSize() — і все це в різних варіантах CamelCase і under_score стилів написання. У підсумку для використання концепції «розмір колекції» нам доводиться циклу for давати знання про реалізацію ось цієї конкретної колекції. А при зміні колекції на іншу — переписувати всі for'и.

8.
for (int i = 0; i < vec.size(); <b>i++</b>)

Тут у нас, звичайно ж, любымый холівар про префиксной і постфиксной формі инкремента. Хочете передраться з колегою і витратити півдня на згадування стандарту мови та вивчення результатів оптимізації коду сучасними компіляторами — ласкаво просимо в старий добрий тред "++i vs i++". Є багато різних місць (і Хабр — одне з них) де про це можна всмак побалакать, але невже ж треба було таким місцем робити третій блок оператора for, що використовується тисячами в кожному першому проекті?

9.
for (;;)

Тут ми маємо теж класичний спір «Та це найефективніший спосіб організації нескінченного циклу!» з «Виглядає бридко, while(true) значно виразніше». Більше холиваров богу холиваров!

10.
for (int i = 0; <b>i++; i < vec.size()</b>)

Цей код компілюється. Деякі компілятори видають warning, але ніхто не видає помилку. Переплутані місцями другий і третій блок не кидаються в очі, оскільки там написані всі знайомі речі — інкремент, перевірка умови. Оператор for виглядає як якийсь апаратний роз'єм, який штекер можна увіткнути і так, і догори ногами, при цьому працювати він буде тільки в одному випадку, а в другому — згорить.

Значна частина подальшої еволюції мов програмування виглядає як спроба виправити for. Мови більш високого рівня (а потім і С++) ввели оператор for_each. Стандартні бібліотеки поповнилися алгоритмами пошуку та модифікації колекцій. С++ ввів ключове слово auto — в основному щоб позбутися від необхідності писати дикі
std::vector::iterator в кожному циклі. Функціональні мови запропонували замінити цикли рекурсією. Динамічні мови запропонували відмовитися від вказівки типу в першому блоці. Кожен спробував якось виправити ситуацію - адже можна було відразу спроектувати краще.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.