Освен на Python, в курса ни във ФМИ се опитваме да учим студентите на „добър код“ (каквото и да значи това). Всеки семестър правим поне една лекция на тема „добри практики“ (спорно с каква успеваемост, понеже такива неща се учат с реални проекти, а не лекции). Искам да споделя любимия си пример.
Иде реч за това как може да направим кода по-ясен (revealing intent). Последователно показвам три различни парчета код, като след всяко питам какво прави. Първото е следното:
loop:
cmpl $12, –4(%rbp)
jge done
leaq _array(%rip), %rax
movslq –4(%rbp), %rcx
movq (%rax,%rcx,8), %rdi
callq _puts
movl %eax, –8(%rbp)
movl –4(%rbp), %eax
addl $1, %eax
movl %eax, –4(%rbp)
jmp loop
done:
Обикновено отговорът е:
Ъ? Ами…
Ако човек няма идея от assembly, декриптирането на този код е загубена кауза. Иначе – изглежда като цикъл и имената _array
и _puts
са подсказващи. Вероятно човек може да се сети, че това е компилираната версия на втория пример:
puts(array[i]);
}
Тук всички дават следния отговор:
Извежда масив с 12 елемента
Съвсем точно.
Интересното е, че няма голяма „концептуална“ разлика между двете. Макар, втория вариант да е по-кратък (и „структуриран“), той казва същото – „извеждаме 12 низа“. Въобще не загатва какви или защо.
Третият пример се получава със съвсем лека промяна:
puts(monthNames[i]);
}
Новият отговор е:
Извежда имената на месеците
Забележете, че само едно нещо е променено (array
→ monthNames
), но отговора вече е на по-високо ниво на абстракция. „Масив с 12 елемента“ звучи като имплементационен детайл, докато „имената на месеците“ е общо-разбираема, непрограмистска концепция.
Това е фин детайл, но разликата е съществена. Изводи колко искаш, но на мен любимият ми е следния: опитвайте да кръщавате нещата на domain-а, вместо на имплементацията.
Aquarius първият пример не е коректен по принцип, 95% от програмистите които познавам си нямат никакъв хабер от assembly също така 99% от програмистите на под 30 годишна възраст. На въпросът защо, нормалният отговор от тях е кому е нужно това имаме си езици от високо ниво. Преполагам и аз щях да съм на същото дередже ако едно време на Имко-1/Правец-82 изборът не беше ограничен между BASIC и Асемблер.
Здравей, според мен примерът започва подвеждащо и абстрактността може да се гледа и от коренно различна позиция.
Самото даване на асемблер на хора, които смятат, че това е нещо сложно е подвеждащо – асемблерът не е сложен.
Това че се отнася точно за месеци го няма в първоначалния код и според мен не е „грешка“ или недостатък на кода – цялостния компилиран код или сорс код на асемблер щеше да съдържа дефиниция на масива (съответно името му и съдържанието) или ако беше от двоичен източник, щеше да включва низова символна таблица или метаданни и адреса от където започва четенето – тогава съдържанието щеше да показва, че в точно този пример това са месеци, дори и да няма идентификатор „месеци“. Същото може да се постигне и с коментар от една дума, така че пак щеше да е ясно приложението на кода:
leaq _array(%rip), %rax ; months
Примерът даден по този начин е като от пример за разнищване на код в обратна посока, при който са повредени символните таблици или се зареждат от някъде (и не могат да бъдат възстановени).
Също така такъв кратък код на асемблер или каквото и да било извън контекст обикновено не съществува, освен като изрезка/макрос/функция с цел именно да бъде обобщен и да не е обвързан с конкретно приложение и конкретни стойности (и фиксиран брой итерации): ако се промени адреса на масива и броя, тогава ще извежда други неща, например 12-те апостоли или 12-те часа или 12-те души от „мръсната дузина“ или „числата от едно до дванайсет на китайски“ – но само ако кодът е обобщен, което в такъв случай щеше да е предимство, защото с по-малко промени ще може да се разширява областта на приложение.
Също така, този код вероятно в асемблер щеше да е част от функция със съответно име, или която като параметър получава какво да печата, print(„months“) и пр. и този цикъл вътре можеше и да не е _array или monthNames, а по-скоро: „puts(a[i])“, от заглавието щеше да е ясно и дългото име можеше да е излишно.
Т.е. ако се разшири обхвата на вниманието, така че да се вижда контекста на асемблерния код, пак щеше някъде да има някой термин от областта на приложение, без да е нужен код на Си.
Бих поспорил за обясненията за абстрактността.
Колкото е по-абстрактен модела, толкова този, който прилага кода трябва да реши за какво да го ползва и да допълни детайлите. А когато се закачат фиксирани имена и фиксирани стойност, не се вдига абстрактността (на модела), а обратното – сваля се, нещата се конкретизират и вкарват в по-определени по-тесни рамки и им се намалява областта на приложение.
Това е добре в определени случаи и за начинаещи, но е зле в други и намалява модулността и възможността за поворно използване на кода.
Добър пример. Аз имам един за performance и като цяло basic познания по алгоритми. https://gist.github.com/AlexanderMitov/9120644 Разликата в кода е само 1-2 реда, но разликата в сложността е огромна – едната функция е със сложност N, а другата 2^N
Както казва Тодор в горния коментар, твърдението, че C кода сам по себе си е с по-висока абстракция е по-скоро некоректно. Разликата между асемблерския фрагмент и съответстващия C код е най-вече в гранулярността. Асемблерския код изразява същото намерение, на същото ниво на абстракция, но с доста по-финна гранулярност, така че да е разбираемо за хардуера. Ако в C кода прекрачиш края на масива те очаква „undefined behavior“ (а не OutOfBoundsException), и именно това е едно от доказателствата, че си на същото ниво на абстракция.
За да си на по-високо ниво на абстракция, трябва да „стъпиш“ на някакъв модел който концептуално е под твоята приложна логика за конкретната ситуация. Подходящ пример за ниско и високо ниво на абстракция са например четене от диска блок по блок vs четене на файл.