Теоретична подготовка
Преди да се втурнем да реализираме "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 долара.
CL-USER>
(Това преди знака за по-голямо е името на текущия package, което е нещо като namespace-ите на C++, C# и package-ите в Java, но затова -- друг път.)
Има нещо изключително важно, което се съзира след внимателен прочит на горното и това е, че всяко нещо, което Lisp може да прочете си има стойност. Именно намирането на тази стойност е задача на оценяването (Eval).
Числата и стринговете са 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 градуса = нула в чашката).
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.
Резултата от изпълнението на defun (т.е. това, което се вижда на 2-рия ред) е името на дефинираната (или ре-дефинираната!) функция. Когато тя се извика (което става на 3-ти ред), формите в тялото й се изпълняват една по една. Резултатът от изпълнението на функцията е стойността на последната изпълнена форма от тялото й. В случая резултата е резултата от извикването на format, т.е. както и преди -- NIL.
Толкова за днес. Лека нощ, драги зрители, дано съм успял да Ви заинтригувам!
Няма коментари:
Публикуване на коментар