събота, 2 март 2013 г.

(здравей-свят)

Имам намерението да ви разкажа в рамките на известен брой публикации за благините на Lisp. Надявам се да ви стане интересно и да получите една различна гледна точка към програмирането. Наясно съм, че конюнктурата не позволява да седнем и да си изкарваме хляба на Lisp в момента -- за съжаление сме "женени" за някакви технологии и няма да получим лесно развод, но това че сме си поръчали, не означава, че не можем да разглеждаме менюто, нали така?

Теоретична подготовка

Преди да се втурнем да реализираме "Hello, World!" не е лошо да вкараме малко теоретична подготовка. Lisp е изведен преди 55 г. и е вторият най-стар език за програмиране от високо ниво -- само FORTRAN е с 1 г. по-стар от него. Като казвам "изведен",  имам предвид именно това -- той е изведен математически и това го отличава от всички останали упражнения по строене на пясъчни кули под формата на езици за програмиране. Хард-кор феновете могат да прочетат оригиналната статия на John McCarthy от 1958 г. тук. Аз няма да й обръщам много внимание, само ще отбележa, че подобно на машината на Тюринг и тук става въпрос за съвсем малък набор от примитиви (ако не ме лъже паметта 7 на брой), чрез които може да се изгради всеки възможен алгоритъм.

Бидейки на 55 г. Lisp е еволюирал дълго, като през това време е дал началото на концепции като виртуална машина, garbage collection, exception handling, та дори и на конструкцията if-then-else. В началото на XXI век има няколко живи диалекта на Lisp, като най-смисленият за мен се казва Common Lisp. Той е стандартизиран от ANSI и е достатъчно освободен (за разлика напр. от Scheme) за да е всъщност полезен.

Lisp е дефиниран като homoiconic език и е първият такъв. На Български това означава, че програмата на Lisp се представя посредством същата структура, с която се представят и данните. Това позволява на дадена Lisp програма да създава, променя и както си иска да манипулира части от Lisp код, вкл. части от своя собствен, без при това да се налага допълнително писане на парсери, генератори на двоичен код, компилатори и т.н. Сега, радетелите за чиста и свята република ще почнат да обясняват колко е вредно да пишем само-модифициращ се код. И сигурно са прави донякъде. Само че ако лежим на тая кълка по-добре да ходим да пасем овцете, защото всяка девелопърска станция без изключение прави точно това: съвкупността от инсталиран софтуер ни позволява да създаваме, променяме и манипулираме други програми, които стават част от инсталирания софтуер. А, и освен това ни позволява да се създаваме, променяме и манипулираме данни по същия начин. С други думи: Lisp изпълнява ролята на цяла (development) машина, от там и термина "виртуална машина". При това всичко се случва по дефиниция, а не защото някой се е сетил (или не се е сетил съответно) да напише компилатор за C и сега се опитва да ни го продаде на промоция за $800 долара.

REPL

Като всяка нормална машина, и виртуалната машина на Lisp, когато я стартираме показва промпт (въпрос към Java фен-бойчетата: защо тяхната виртуална машина не прави така?). Има и един друг съвременен език, по който хората напоследък са луднали, който прави същото: нарича се Python. На Lisp-а промпта се нарича REPL и е съкращение от Read-Eval-Print-Loop -- т.е. прочитаме какво иска да ни каже потребителя (Read), изпълняваме го (по-правилния термин е оценяваме го) (Eval) и изпечатваме резултата (Print); след това почваме от начало (Loop). Просто като боб. Ето как изглежда REPL промпта обикновено:

CL-USER>

(Това преди знака за по-голямо е името на текущия package, което е нещо като namespace-ите на C++, C# и package-ите в Java, но затова -- друг път.)

Има нещо изключително важно, което се съзира след внимателен прочит на горното и това е, че всяко нещо, което Lisp може да прочете си има стойност. Именно намирането на тази стойност е задача на оценяването (Eval).

Форми и оценяването им

Програмите/данните на Lisp се състоят от неща, които се наричат форми. На най-ниско ниво Lisp разбира от два вида форми: атоми и списъци. Атомите са единични неща, като напр. числото 5, стринга "БАХОР" или името PI (терминът, който се използва в Lisp за означаване на име е символ (symbol). Да не се бърка със символ в смисъла на char). Списъците от своя страна се записват по възможно най-простия начин: като поредица от атоми заградени в кръгли скоби (например този наклонен текст тук в скобите е списък).

Числата и стринговете са self-evaluating, т.е. резултата от оценяването им е присъщата им стойност:

CL-USER> 5
5
CL-USER> "БАХОР"
"БАХОР"

(Тук 2-ри и 4-ти ред показва резултата от Print частта на REPL).

Когато става въпрос за символ, като напр. PI, резултата е стойността, която в момента е свързана с това име:

CL-USER> PI
3.141592653589793d0

Свързването (binding) прилича малко на присвояването (assignment), но не е същото, но затова друг път. За целите на настоящия пост е достатъчно да си мислим, че в случая променливата PI има стойност 3.141592653589793d0, която Eval частта на REPL е намерила и съответно Print частта е изпечатала.

Интересното става, когато трябва да се оцени стойността на списък. Каква е стойността на списъка (1 2 3 4) ? Или пък на списъка (трябва да пия кафе сега) ? Тук се процедура по следния начин: първия елемент от списъка трябва да е символ и той се разглежда като име на функция. Ако такава функция има дефинирана, тя се извиква, като останалите елементи от списъка се разглеждат като аргументи на фукцията и се оценяват (по същите тези правила), един по един и отляво надясно. Стойността на списъка е стойността, която функцията връща. Например:

CL-USER> (cos (/ pi 2))
6.123233995736766d-17

Най-напред имаме списък от 2 елемента: първият е символа cos, а вторият е списъка (/ pi 2). Първият елемент се разглежда като име на функция. Има ли такава функция? Има, да, както се сетихте това е функция с един аргумент, имащ смисъла на ъгъл в радиани, и нейната стойност е косинуса от този ъгъл. След като има дефинирана такава функция, остава да я изпълним. За целта обаче трябва първо да се оценят аргументите й, т.е. останалите неща в списъка, а това е от своя страна списъка (/ pi 2). Първия елемент от този списък е символа /, а втория и третия са съответно името pi и числото 2. Има ли дефинирана функция / -- има, тя дели първият си аргумент на всички останали аргументи. Остава да оценим нейните аргументи. Те са атоми и следователно се оценяват както е описано по-горе. Делението се изпълнява и резултата от него PI/2 се използва като аргумент за функцията cos. Сега от своя страна се изпълява и тя и резултата е някакво много близко до нула число (както знаем от Студ. град -- cos от 90 градуса = нула в чашката).


Вариации на тема "Здравей, Свят!"

Първи вариант:

CL-USER> "Здравей, Свят!"
"Здравей, Свят!"

Нищо ново -- Lisp прочете стринга, който му написах, оцени го като самия себе си и ми го изпечата обратно.

Втори вариант:

CL-USER> (format t "Здравей, Свят!~%")
Здравей, Свят!
NIL

Това, което се случва тук, е функцията format. Тя има същата роля както fprintf в C. Първия й аргумент е stream, към който да изпрати изхода. В случая със символа T (който по дефиниция е свързан с булевата стойност True) се указва на функцията да прати резултата на стандартния изход. Следващия аргумент е форматиращия стринг, нищо по-сложно от fprintf. Това, което е по-различно е как се задават полета за печат. Тук, за разлика от C-то не се задават с процент (%), а с тилда (~). Това, което се вижда на края на стринга (~%) е еквивалента на \n в C. Изключително мощна е конструкцията ~A, която печата съответния аргумент в естетически формат, т.е. така, че да е удобно да бъде прочетен от човек.

Прави впечатление, че след изпълнението на функцията се появяват 2 реда. Това е така, защото първия ред е изхода, генериран от функцията и изпратен на стандартния изход, а втория ред е самата стойност на функцията, т.е. резултата от Eval-а (в случая този резултат е NIL -- символ, обратен на T, който по дефиниция е свързан с булевата стойност False). Тук се наблюдава т.н. страничен ефект -- функцията повлиява на обкръжението си освен със стойността си, и по друг начин (в случая изпечатването на стринг на екрана). В някои по-рестриктивни диалекти на Lisp, като например Scheme, страничните ефекти са забранени. Това обаче не е продуктивно и затова към Scheme има по-скоро само академичен интерес.

Трети вариант:

CL-USER> (format t "~A~%" "Здравей, Свят!")
Здравей, Свят!
NIL

Същото като преди, само че този път във форматиращия стринг е използвана конструкцията ~A, която печата стойността на съответния аргумент, в случая стойността на стринга "Здравей, Свят!". Това е едно към едно с fprintf(stdout, "%s\n", "Здравей, Свят!") на C.

Последен вариант:


CL-USER> (defun здравей-свят () (format t "Здравей, Свят!~%"))
ЗДРАВЕЙ-СВЯТ
CL-USER> (здравей-свят)
Здравей, Свят!
NIL

Ето до това трябваше да стигнем. На първия ред изпълняваме функцията defun, която дефинира нова функция (нали се сещате -- малко по-нагоре, в теоретичната подготовка -- "...позволява да се създава, променя и манипулира код..."). Първият й аргумент е името на новата функция, в случая здравей-свят. Усещам как любителите на camelCase-а почват да се въртят като обрани евреи. Безспорно от трите имена

locate_image_and_perform_stuff_on_it
locateImageAndPerformStuffOnIt
locate-image-and-perform-stuff-on-it

последното е най-четимо. В Lisp практически няма (е, почти няма) ограничение за буквите и символите, които могат да присъстват в имената на променливи и функции. Затова и мога да дефинирам функция с име на Български, че ако искам и на Японски:

CL-USER> (defun 世界ようこそう () (format t "世界、ようこそう!~%"))
世界ようこそう
CL-USER> (世界ようこそう)
世界、ようこそう!
NIL

Втория  аргумент на defun е списък с аргументи за новата функция. В случая тя няма такива, затова списъка е празен. Останалите елементи от списъка на defun формират тялото на новата функция.

Резултата от изпълнението на defun (т.е. това, което се вижда на 2-рия ред) е името на дефинираната (или ре-дефинираната!) функция. Когато тя се извика (което става на 3-ти ред), формите в тялото й се изпълняват една по една. Резултатът от изпълнението на функцията е стойността на последната изпълнена форма от тялото й. В случая резултата е резултата от извикването на format, т.е. както и преди -- NIL.

Толкова за днес. Лека нощ, драги зрители, дано съм успял да Ви заинтригувам!

Няма коментари: