четверг, 13 января 2011 г.

Python. Хочу DSL.

Многие рубисты, джависты, лиспофанаты etc очень любят говорить о DSL.
Что такое DSL? Почитайте в википедии, гугле или ещё где. Если говорить коротко, то это специфичные для определённой задачи языки, которые отбрасывают сложности в описании и решении задачи.

create page
   name 'Hey'
   date 11-01-2011
   text 'How about new DSL?'

Не правда ли красиво? Ну это всяко красивее аналога на чистом питоне. Похоже на yaml, но одного yaml'а здесь явно не хватит, нужна поддержка функций, интерпретация, более глубокая семантика. На руби такой код составить будет не сложно. Это практически валидный руби-код.
Конечно, на руби точно такой код не получить. Добавится end, дату нужно будет иначе описывать...

На питоне это можно оформить так:
p = Page()
p.name = 'Hey'
p.date = '11-01-2011' или datetime.datetime.from....
p.text = 'How about new DSL?'

Ужас, не так ли? Голая логика, строгость и КОДИНГ. Можно и иначе, с помощью with. Однако, нужно будет описывать __enter__, __exit__, что заставит наш код быть ориентированным на работу с этим оператором, что излишне.

Увы, на питоне нет средств для написания DSL. Или есть?


Прежде всего, DSL бывают разные. Внутренние(internal) и внешние(external). Оба типа DSL являются относительными.

Грубо говоря, внутренние - сделанные средствами самого языка, используемые внутри самого языка. Типичные DSL на руби и лиспе(я промолчу на счёт джавы, ибо не изучал этот вопрос на ней) являются внутренними DSL, которые строятся через дополнение синтаксиса средствами самого языка. Фактически, в лиспе это не является дополнением синтаксиса, как и в руби. Просто добавление ключевых слов, функций, логики... Словом, просто пляски вокруг синтаксиса, используя возможности самого же языка. Язык сильно ограничивает возможности по создания внутреннего DSL.
Примерами внутренних DSL являются десятки DSL для Ruby on Rails, огромное число пакетов тестирования для руби, питона, HAML, SASS, CleverCSS, десятки диалектов лиспа etc.
В наше время каждый современный программист использовал DSL или хоть пробовал данную идею.

Внешние DSL - язык, созданный "в лоб". Для него делается свой интерпретатор/компилятор, синтаксический парсер etc. К таким языкам часто относятся языки темплейтов в веб-фреймворках. По сути, это изобретения велосипеда. Mako(язык темплейтов, так популярный в веб-фреймворке Pylons), например, является транслятором в Python. Да-да, код транслируется в код на питоне(а точнее, в один модуль с одной функцией).

Чаще всего выходит так, что для одной цели, где нужен внутренний DSL, задача решается "в лоб" из-за незнания или иных проблем. Это приводит к созданию нового языка со своими конструкциями. Не редко язык получается уродливым, да и задача решается не так как хотелось бы. Ладно если язык компилируемый, но если он интерпретируемый, то это создаёт оверхед(во всех аспектах программирования) такого размера, что выть хочется.

Ладно, давайте ближе к делу, а именно, к DSL на Python.

В Питоне нет функционала для создания DSL. Гвидо официально пофиг(ищите списки рассылок, вопрос поднимался много раз, но темы всегда заминали). Более того, язык перестал развиваться. Однако, он всё ещё является лучшим языком, с явно бОльшим будущем чем у руби. Почему? Py3k с каждым релизом становится всё быстрее и быстрее. Хотя хватит рекламы. CPython 3 всё-равно говно, но лучше нет.
У Питона пока 2 проблемы:
1) Замороженный синтаксис(да, он просто уже не развивается).
Более того, в тройке он деградировал(вроде отсутствия "%").
2) Плохая реализация(да, она просто говно).
Все надежды на PyPy, Unladen Swallow, но первый пилят уже слишком долго, а второй хотят прикрыть, т.к. не подходит для гугла.

А ещё есть джава. А ещё есть эрланг. А ещё есть лисп.
Но есть только один достаточно хороший для 2010ого(а уже 2011ый!) года - Python.

Ладно, это уже может пахнуть фанатизмом, нам нужен DSL! Мы хотим сделать Python совершенным, а для этого нужно просто получить возможность делать DSL, сделать блоки кода объектами, убрать подчёркивания в некоторых методах, сделать приватные члены не через подчёркивание...

Гугл сразу даёт массу ссылок на инструменты по созданию external DSL: PyParsing, Ply, PySEG, Pysec... Их действительно много.
Если бы я делал свой язык, то я бы использовал что-то из этого списка, но для DSL это всё не может подходить(не для таких DSL, которые нам нужны).

А что нам нужно?
Добавить пару своих операторов, обрабатывающих операнды через нашу логику.
Добавить пару спец. символов.
Немного изменить логику работы со стандартными типами(обернуть строки в наш специальный класс, например).
Получить возможность как можно более простого описания DSL, который будет справляться с такими задачами.
etc.

Чего мы не хотим?
Писать свой язык.
Копаться в кишках чего-то.
Изучать лексеры.
Превышать среднюю концентрацию символов "_" более чем на 2 символа на 3 строчки кода(шутка, хоть и есть доля правды).
Создавать оверхед, рантаймовый препроцессор или что-то в этом духе.
etc.

А сейчас важная деталь, которая необходима для понимания смысла DSL, что это такое, зачем это всё: Питон это ТОЖЕ DSL; любое небольшое вмешательство позволяет говорить что ты сделал свой DSL.
Почитайте вот это письмо из рассылки, в котором есть информация о том, как в питоне "реализованы" DSL.
В письме есть ссылка на "DSL для графов".
На самом деле там нет ни слова о DSL, но указывается на удобство Питона для таких задач.
А ещё там замечательная ссылка на туториал с пикона. Но, как и все туториалы с пикона, демонстрации, показы, презентации etc, их можно увидеть только один раз. Передаю струйку рака в сторону кодеров, которые не заботятся о том, чтобы их презентации были выложены в сеть.


Ладно, хватит о мелочах, о частностях. У нас есть цель, задача, а самое главное - вот такой вот интересный Я, который покажет вам что можно юзать для таких задач.

К слову, есть мнение, что DSL делается безумно просто и на Питоне, ровно как и на Руби. Однако, нет примеров, но есть такие слова как "Zope", "Django", "TG2". Да, в них есть DSL'и, но они относятся к мелким добавкам к основному языку. Ничего интересного.

Ладно, я слишком много рассуждал, тянул время, заставлял ждать. Думаю, вы готовы.

http://fmeyer.org/en/writing-a-DSL-with-python.html

Наверно, самый красивый вариант написания DSL на Питоне. Самый красивый, костыльный, простой, идеально подходящий.
Пока точно не могу сказать что именно его буду использовать, но, как мне кажется, лучше не найду решения.

Для начала, вот живой код, который должен, будет работать:

# coding: pyspec
class Bow:
    def shot(self):
        print "got shot"

    def score(self):
        return 5

describe Bowling:
    it "should score 0 for gutter game":
        bowling = Bow()
        bowling.shot()
        assert that bowling.score.should_be(5)

А теперь небольшое отступление.
Помните что нужно делать, чтобы код на питоне воспринимался как utf8? Так вот, это не магия, это большая система, которая хорошо продумана. Называется она "кодеки".
Благодаря наличию такой системы, мы можем получить доступ к ТОКЕНАЙЗЕРУ Питона, к ридеру кода! Мы можем управлять тем, как код понимается компилятором в pyc и интерпретатором! Да-да!


Обратите внимание на первую строчку кода. Мы указываем что хотим использовать несуществующий кодек - кодек, который мы напишем. Именно этот кодек позволит обрабатывать нестандартные операторы, изменять логику работы.

Весь код реализации можно смотреть по ссылке выше, нам же интересно то, что мы будем писать сами и понять как это всё работает.

Структура кодека содержит поля: name, encode, decode, incrementalencoder, incrementaldecoder, streamreader, streamwriter. Нам важно только одно - переписать streamreader чтобы можно было задействовать свои хуки.
Собственно, все действия сводятся к созданию хендлера для поиска кодека, в котором мы возвращаем наш новый кодек, если имя совпадает. Кодек создаём передавая все поля как у utf8-кодека, но вот streamreader мы делаем свой.

import tokenize
import codecs, cStringIO, encodings
from encodings import utf_8

class StreamReader(utf_8.StreamReader):
        def __init__(self, *args, **kwargs):
            codecs.StreamReader.__init__(self, *args, **kwargs)
            data = tokenize.untokenize(translate(self.stream.readline))
            self.stream = cStringIO.StringIO(data)

Пример не самый красивый, но стоит того, чтобы его прокомментировать.
Про модуль токенайзера можете прочитать в доках, если что.
Мы создаём функцию translate, которая является генератором строк, которые берутся из генератора же(stream.realline).
Мы берём оригинальный поток строк, передаём его в нашу функцию, обёртываем в токенайзер и ставим это на место старого стрима. Получается что-то вроде:

поток из файла -> (токенайзер -> наш транслятор) -> валидные питонотокены -> интерпретатор/компилятор

Вот как будет выглядеть наш "транслятор":

def method_for_it(token):
    return token.strip().replace(" ", "_").replace("\"","" ) + "(self)"
 
def translate(readline):
    previous_name = ""
    for type, name,_,_,_ in tokenize.generate_tokens(readline):
        if type ==tokenize.NAME and name =='describe':
            yield tokenize.NAME, 'class'
        elif type ==tokenize.NAME and name =='it':
            yield tokenize.NAME, 'def'
        elif type == 3 and previous_name == 'it':
            yield 3, method_for_it(name)
        else:
            yield type,name
        previous_name = name

Возможно, это не самый красивый вариант, который можно придумать, но какой есть и можно использовать.
На днях попробую сделать небольшой инструмент для создания DSL используя кодеки.

А теперь давайте посмотрим на pros/cons этого метода.
+ Возможность написания внутренних DSL.
+ Возможность изменять готовое, а не писать с нуля.
+ Возможность использовать всё тот же питон.
+ Отсутствие необходимости писать всё и вся с нуля.
+ Использование родного, встроенного в Пайтон функционала.
+/- Гибкость. Не самый гибкий метод, но позволяет делать многие вещи, которые нельзя в руби.
- Работа по принципу "read&replace". Мы работаем со строками!
- Слабый дебаг. Велика вероятность что если будет ошибка, то её сложно будет найти, т.к. питон будет выкидывать пост-парсинговый код, а не код на DSL.
- Сложность описания DSL.
- Необходимость описания небольшой, более удобной обёртки.
- ЭТО КОСТЫЛЬ! Кодеки не для этого предназначены.

В любом случае, лучше варианта нет, а если использовать активно и пропагандировать, то можно будет надеяться что Гвидо прогнётся под коммьюнити и даст нам хороший инструмент.

В заключение хочется вбросить одну ссылку на интересную мысль на счёт DSL на Пайтоне, а точнее на применение DSL для ORM на Пайтоне.
http://blog.ianbicking.org/more-on-python-metaprogramming.html

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

  1. Да, код получается не очень красивым. Поэтому захотелось написать на нем такой DSL, который бы позволил писать свои произвольные DSL. Ха!

    ОтветитьУдалить
  2. Я сейчас занимаюсь таким инструментом, очень красиво выходит. Читай http://blog.soulrobber.ru/ , я перешёл туда и там же, скоро, опишу процесс создания и работу моего "менеджера DSL".

    ОтветитьУдалить
  3. Кстати, подобным же образом работают некторые темплейтные движки, компилирующие шаблоны в питон.

    ОтветитьУдалить