Неведнъж са ми се чудели защо се занимавам със всякакви екзотични езици за програмиране. Има ред причини, и учудващо, всички са практически. „Учудващо“, защото приложението не е очевидно – докато няма скоро да пиша production на Io, научаването му направи Ruby кода ми по-добър. Дори преди години имаше подобна серия от блог постове – Как език X ме направи по-добър програмист в Y. Друг е въпросът дали няма по-ефективен начин от постоянното учене на езици. Това настрана, искам да разкажа за един интересен аспект на „програмистската полиглотност“, за който ще направя паралел с математиката.
В дебрите на (математическата) история
Макар числата да са ни известни от много отдавна, изглежда древните гърци поставят началото на математиката като наука. Сектата на Питагор, Евклид, Архимед и други са прекарали голяма част от живота си в „измислянето“ на алгебрата и геометрията, които сега се изучават в основното училище. Интересно е, че за цялата математиката до края на Средновековието в момента се изучава в него. Деца се справят успешно с неща, с които възрастни хора от „едно време“ са се измъчвали. Около Нютон и Лайбниц нещата започват да се променят и вече развиваме по-сложна математика. През следващите 200-300 години „избухваме“ с теория, която озорва дори сегашните магистри.
Можете ли да познаете каква промяна превръща математиката от нещо трудно и философско, в нещо, което десет годишно дете може да научи?
Нотацията. Най-същественото нещо за развитието на математиката е създаването на удобна нотация. Например, древните гърци не са разполагали с удобен запис на прости отношения. Евклид би казал „токът е право-пропорционален на напрежението и обратно пропорционален на съпротивлението“ (сметките за електроенергия са били сред честите теми за разговор из пазарите на Елада). Може да си представите колко по-трудно им е било. Съвременният човек е далеч по-ефективен:
$$ I = \frac{U}{R} $$
Разликата е дори по-голяма. До края на Средновековието са се ползвали римски цифри (древните гърци са ползвали подобен запис). Само вижте как се прави аритметика с римски цифри. Събирането и изваждането са играчка, умножението е зор, а делението си е направо хвърляне на боб. Нищо чудно, че римляните са имали нужда от сметало.
И нищо чудно, че математиката започва да се развива чак когато започваме да използваме арабски числа и подходяща нотация.
Защо нотацията е важна?
Виждам две причини. Първата е психологическа (neuroscientific?) – мозъкът ни се справя добре с написани символи и не-толкова-добре с абстрактни идеи, изразени в думи. Добрата символна система е основната причина да се справяме добре с писмен език. Когато нямаме добра нотация, създаваме едно (или няколко) междинни нива.
Ще се опитам да дам пример. Да вземем низа „ди оу джи“. Тук има няколко слоя на траснлация. Имаме (1) транскрибиране на кирилица на (2) три звука, които представляват (3) букви от латинската азбука, (4) образуващи дума. Далеч по-лесно схващате смисъла, ако напиша „dog“. При кратки думи не е толкова голям проблем, но ако напиша цял параграф (или блог пост?) по този начин, ще видите голям зор да стигнете до смисъла. Вероятно бързо ще се откажете просто да четете. Вместо това сигурно ще преведете от кирилското изписване до латински букви и после да прочете текста на английски. А ако не знаете латинската азбука? В подобно положение са били древните гърци.
Втората причина е прагматичност. Докато арабски числа се събират лесно наум, то колко е CCCLXXIX плюс CDLXIII? Ако тръгнете да го смятате, вероятно първо ще обърнете до арабски цифри, ще направите събирането с тях и ще обърнете резултата обратно до римски. Това е доста по-прагматичен начин да стигнете до отговора, DCCCXLII. Уви, римляните са виждали повече зор.
Обратно към програмирането
Препрочитам K&R и една задача ми направи впечатление:
Напишете програма, която замества всички табулации в началото на реда с четири интервала
Понеже това е C книга, нека да видим как изглежда решението на C:
int main() {
int bol, c;
bol = 1;
while ((c = getchar()) != EOF) {
if (bol && c == ‘\t‘) {
printf(" ");
} else if (c == ‘\n‘) {
bol = 1;
putchar(c);
} else {
bol = 0;
putchar(c);
}
}
return 0;
}
Обърнете внимание на нивото на абстракция. Трябва ясно да моделираме самото изчисление, както и да поддържаме състояние (bol
, което ми се стори подходящо C име за „beginning of line“). Има ред малки детайли, в които няма да задълбавам (като представянето на булева стойност като цяло число).
Това не е лош код. Определено ще е бързичък. Може да се подобри по няколко начина. Например set-ването на bol
на всеки символ може да се избегне и (вероятно) кода да стане по-бърз (с точност до компилатор). От stdbool.h
може да вземем булев тип, което ще го направи малко по-ясен.
Ето и версия на Perl:
s/^\t+/’ ‘ x length($&)/e;
print;
}
Две сериозни подобрения. Първо, обхождането по редове е изразено по-конкретно (с по-добра нотация) като while (<>) { ...; print; }
. Второ, регулярните изрази изразяват табулациите в началото на реда по-ясно.
От друга страна, ако не знаете Perl, неща като <>
и x length($&)
не са съвсем очевидно. Да не говирим, че сигурно ще се чудите защо print
не взема аргумент.
Ами Ruby?
puts line.gsub(/^\t+/) { |ws| ‘ ‘ * ws.length }
end
Една идея по-експлицитно, дори да е проста „транслитерация“ към Perl кода.
Нищо не ни пречи да ползваме while
/getc
цикъл в Ruby, нито да ползваме библиотека за регулярни изрази в C. Въпреки това, кода по-горе е „идиоматичен“ – този, който опитен програмист на езика би написал. Съответно, можем да кажем (с известни уговорки), че това е начина, по който „се мисли“ на тези езици.
Разбирането на смисъла също е интересно. В Ruby (и Perl) от кода е относително очевидно какво прави програмата (ако знаете Ruby (и Perl)), докато в C трябва да я „интерпретираме наум“ за да стигнем до извод. С известна неточност, може да кажем, че последните две програми са „по-декларативни“, докато първата – „по-императивна“.
Всичко тук е следствие от по-удобна нотация.
Обратно към многото езици
Всеки език, с идиомите си, ни научава на нов начин за изразяване. Понякога това помага да разберем по-добре нещо в „родния“ език (например Smalltalk ми помогна да схвана ООП-то в Java и design pattern-ите). Понякога не показва как нещо не-толкова-централно може да се окаже безценнен инструмент (Scheme ме научи на map
, select
и reduce
). Понякога показват как да решим проблеми, за които „родния ни език“ не е проектиран. При всички случаи, научаваме нов подход.
И една идея по-нагоре, развиваме по-качествен начин на мислене за такива проблеми. Само защото друг език е подбрал различна нотация.
Бележки под линия
- Не искам да кажа, че нотацията на Ruby и Perl е строго по-добра от тази на C – просто съм подбрал такъв проблем. Има по-low-level проблеми, които на C ще бъдат изразени по-ясно.
- Паралела на математическата нотация могат да бъдат много неща – синтаксис, стандартна библиотека или дори начина, по който проблема в този език се решава (структури vs. обекти).
- Както с математиката, трябва да познаваш нотацията, за да можеш да я разбереш. изглежда като гръцки, ако не си се занимавал с асимптотична сложност.
- Едни нотации са „по-дружелюбни“ от други – има по-голям шанс да разберете Ruby кода, отколкото Perl кода, ако двата езика са ви непознати.
- Понякога нотацията изисква познание на модела. Например в Perl има аргумент по подразбиране (
$_
), който свързва трите реда от програмата. Ако не знаете това, трудно ще разберете какво се случва.
Между другото се оказва че Архимед е бил разработил част от математическия анализ преди Нютон и Лайбниц.
http://en.wikipedia.org/wiki/Archimedes_Palimpsest