Очарователен Python, създаващ декларативни мини-езици
Програмирането като изявление, а не като инструкция
Серия съдържание:
Това съдържание е част # от поредица # статии: Очарователен Python
Това съдържание е част от поредицата: Очарователен Python
Очаквайте нови статии от тази серия.
Когато повечето програмисти мислят за програмиране, те мислят за императивни стилове и технологии, когато пишат приложения. Най-популярните езици за програмиране с общо предназначение - включително Python и други обектно-ориентирани езици - са предимно императивни по стил. От друга страна, има много езици за програмиране, които са декларативни по стил, включително функционални и логически езици, както и специализирани езици и езици с общо предназначение.
Езици като граматиките на Prolog, Mercury, SQL, XSLT, EBNF и наистина конфигурационни файлове с различни формати, всички декларират, че съществува ситуация или че се прилагат определени ограничения. Функционалните езици (като Haskell, ML, Dylan, Ocaml, Scheme) обаче са подобни, с по-голям акцент върху формулирането на вътрешни (функционални) връзки между програмни обекти (рекурсия, списъци и т.н.). Нашият обикновен живот, поне неговата описателна страна, няма пряк аналог на програмните конструкции на тези езици. Въпреки това, за проблеми, които можете лесно да опишете на тези езици, декларативните описания са много по-сбити и много по-малко податливи на грешки от императивните решения. Помислете например за системата от линейни уравнения:
Това е много елегантна нотация, която установява връзки между обекти (x, y и z). Може би тиизправен пред тези факти в различни ситуации от реалния живот, но всъщност намирането на x ръчно, на лист хартия, е усърдна работа, изпълнена с грешки. Но писането на тези стъпки в Python може би е още по-лошо - от гледна точка на отстраняване на грешки.
Prolog е език, който се доближава до логиката и математиката. В него просто записвате твърдения, за които знаете, че трябва да са верни, и след това молите приложението да направи изводи вместо вас. Твърденията са написани от всяка конкретна последователност (точно както линейните уравнения нямат ред) и програмистът/потребителят няма представа какви стъпки са включени в получаването на резултатите. Например:
Това е компактен начин да определите как може да изглежда една дума, ако я срещнете, без всъщност да предоставяте последователни инструкции как да я разпознаете. Това е подобно на регулярните изрази (и всъщност те са достатъчни за тази конкретна граматична продукция).
Както при други примери, езикът DTD не включва никакви инструкции какво да направите, за да разпознаете или създадете валиден XML документ. Той просто описва какъв би могъл да бъде документът, ако съществуваше. Декларативните езици се характеризират с подчинително настроение.
Python като интерпретатор срещу Python като среда
DTD е подобен на останалите примери. Ако използвате валидиращ парсер като xmlproc, можете да използвате DTD, за да проверите диалекта на XML документа. Езикът DTD обаче е "не-Pythonic" и xmlproc просто го използва като данни за анализиране. Освен това валидиращите XML анализатори са написани на много езици за програмиране. По същия начин трансформациятаXSLT е "не-Pythonic" и модул като ft.4xslt просто използва Python като лепило.
Магията на интроспекцията
Анализаторите на Spark и PLY позволяват на потребителите да декларират стойности на Python в Python и след това да използват малко магия, за да позволят на средата за изпълнение на Python да действа като конфигуратор за анализиране. Помислете например за PLY еквивалента на предходната граматика на SimpleParse. (примерът за Spark е почти същият):
Този стил не би бил по-малко декларативен и модулът basic_lex би могъл хипотетично да съдържа нещо толкова просто като:
Това ще генерира:
PLY успява да проникне в пространството на имената на импортиращия модул, използвайки информацията за рамката на стека. Например:
Има повече магия в действителния PLY модул. Видяхме, че токените, обозначени с шаблона t_TOKEN, всъщност могат да бъдат или низове, съдържащи регулярни изрази, или функции, които включват документационни низове за регулярен израз заедно с действителния код. Някакъв тип проверка осигурява полиморфно поведение:
Разбира се, истинският PLY модул прави нещо по-интересно, използвайки демонстрираните подходи, отколкото тези примери за играчки, но те също демонстрират някои от включените технологии.
Магията на наследството
Като се позволи на поддържащата библиотека да манипулира пространството от имена на приложението, може да се приложи елегантен декларативен стил. Но често използването на наследствени структури заедно с интроспекция позволява още повече гъвкавост.
Модулът gnosis.xml.validity е библиотека за създаване на класове, които се картографират директно към получените DTD. Всеки клас gnosis.xml.validity може да бъде създаден само с аргументи,предмет на ограниченията на XML диалекта. Всъщност това не е съвсем вярно; този модул също ще изведе правилни типове от по-прости аргументи, ако има само един последователен начин за "изграждане" на типа в правилно състояние.
Тъй като сам написах модула gnosis.xml.validity, съм склонен да мисля, че целта му е интересна сама по себе си. Но в тази статия искам само да разгледам декларативния стил, в който са създадени класовете за валидност. Наборът от правила/класове, съответстващи на предишния DTD шаблон, се състои от:
Забележете колко точно тези класове съвпадат с предишния DTD. Това съпоставяне е основно едно към едно, с изключение на това, че междинните имена трябва да се използват за определяне на броя и редуването на вложените тагове (междинните имена са маркирани с водеща долна черта).
Също така имайте предвид, че когато са създадени с помощта на стандартен синтаксис на Python, тези класове са необичайни (и по-сбити), тъй като нямат нито методи, нито данни за екземпляри. Класовете се дефинират единствено, за да наследят някаква структура и тази структура е ограничена до един атрибут на класа. Например, е поредица от други тагове, а именно: последвано от един или повече тагове
. Но всичко, което трябва да направим, за да наложим това ограничение върху инстанцията, е просто да декларираме главата на класа.
Основният "трик", използван при програмиране на родителски класове като gnosis.xml.validity.Seq, е да се вземе предвид атрибутът .__class__ на екземпляра по време на инициализация. Класът chapter няма собствена инициализация, така че се извиква методът __init__() на неговия родител. Но себе си се предава на __init__() на родителя,е екземпляр на chapter и го знае. Като илюстрация, помислете за фрагмент от изпълнението на gnosis gnosis.xml.validity.Seq:
Ако разработчикът на приложението се опита да инстанцира chapter, кодът за внедряване гарантира, че chapter е деклариран с необходимия атрибут на класа ._order и този атрибут е кортеж. Методът .validate() извършва още няколко проверки, за да гарантира, че обектите, с които този екземпляр е инициализиран, принадлежат към подходящите класове, посочени в ._order.
Кога да обявим
Декларативният стил на програмиране почти винаги е по-директен начин за определяне на ограничения от императивния или процедурния. Разбира се, не всички програмни проблеми включват ограничения - или поне не винаги естествено формулирани в такива термини. Но проблемите на системите, базирани на правила, като граматики и системи за извод, са много по-лесни за решаване, ако могат да бъдат описани декларативно. Императивната проверка на граматиката бързо се превръща в труден за отстраняване на грешки спагети код. Твърденията на шаблони и правила могат да бъдат много по-прости.