PiterPy#3. DSL in Python. How and why?

PiterPy#3. DSL in Python. How and why?

My talk is about DSLs, their kinds and when it’s worth to be using them. I’ll also demonstrate different approaches to developing internal and external DSLs in Python and will try to give the comparative analysis of those.

15563b4bb24076f1801cd862f74ed3fe?s=128

Ivan Tsyganov

April 22, 2016
Tweet

Transcript

  1. DSL в Python. Цыганов Иван Positive Technologies Как и зачем?

  2. – Мартин Фаулер Предметно ориентированный язык — это язык программирования

    с ограниченными выразительными возможностями, ориентированный на некую конкретную предметную область
  3. SQL SELECT * FROM Users WHERE Age>20

  4. SQL SELECT * FROM Users WHERE Are>20 REGEXP [A-Z][A-Za-z0-9]*

  5. SQL REGEXP [A-Z]\w+ LaTeX E &=& mc^2\\

  6. SQL REGEXP LaTeX E &=& mc^2\\ HTML <a href='http://piterpy.ru'>PiterPy</a>

  7. SQL REGEXP LaTeX HTML <a href='http://piterpy.ru'>PiterPy</a> PonyORM select(p for p

    in Person if p.age > 20)
  8. SQL REGEXP LaTeX HTML PonyORM WTForms class UsernameForm(Form):
 username =

    StringField('Username')
  9. ✤ SQL ✤ REGEXP ✤ TeX/LaTeX ✤ HTML Виды DSL

    DSL Внутренние Внешние ✤ PonyORM ✤ WTForm ✤ Django models
  10. Высокая скорость разработки import re
 
 html_source = '''...'''
 


    links = re.findall(
 pattern=r'''<a href=("|')(?P<URL>.*?)\1>(?P<Text>.*?)</a>''',
 string=html_source
  11. Дополнительный уровень абстракции persons = select(p for p in Person

    if p.age > 20)[:]
  12. Код могут писать "не-программисты" server { location / { proxy_pass

    http://localhost:8080/; } location ~ \.(gif|jpg|png)$ { root /data/images; } }
  13. Проблемы и решения Высокая стоимость разработки DSL

  14. Проблемы и решения Достаточно один раз разобраться Высокая стоимость разработки

    DSL
  15. Проблемы и решения Достаточно один раз разобраться Высокая стоимость разработки

    DSL Нет специалистов со знанием языка
  16. Проблемы и решения Достаточно один раз разобраться Высокая стоимость разработки

    DSL Язык должен иметь ограниченные возможности Нет специалистов со знанием языка
  17. Проблемы и решения Достаточно один раз разобраться Высокая стоимость разработки

    DSL Язык должен иметь ограниченные возможности Нет специалистов со знанием языка Работает не для всех задач
  18. Проблемы и решения Достаточно один раз разобраться Высокая стоимость разработки

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

  20. Зачем нужна модель ✤ Хранит в себе всю бизнес-логику ✤

    Позволяет использовать различные DSL ✤ Обеспечивает возможность тестирования
  21. Семантическая модель

  22. Внутренние DSL

  23. Внутренние DSL Все возможности базового языка Привычный синтаксис Легко работать

    Ограничен базовым языком
  24. Цепочки вызовов ✤ Все методы заполняют модель и возвращают объект

    ✤ Методы именуются исходя из смыслового контекста FileUpdater()\
 .path('\music')\
 .mask('.*metallica.*')\
 .set(Genre='Rock')\
 .set(Artist='Metallica'\
 .do()
  25. Вложенные функции ✤ Для заполнения модели вызываются функции ✤ Функции

    именуются исходя из смыслового контекста update(
 settings(
 path('./music'),
 mask('.*\.mp3')
 ),
 set(Artist='Metallica'),
 set(Genre='Rock')
 )

  26. Import hooks Очень мощный инструмент Вмешиваемся в работу интерпретатора Очень

    сложная отладка Непредсказуемые side- эффекты
  27. Import hooks Не используйте это в реальном мире!

  28. PathFinder FileLoader import requests def source_to_code(self, data, path, *, _optimize=-1):


    … source = self.get_source(path)
 return compile(
 source, path, …
 )
  29. WITH ".*\.mp3"
 IN "../tests/music/"
 SET Artist="Metallica"
 SET Genre="Rock" my_script.py import

    internal.import_tokenizer
 import examples.internal_data.my_script as script
 
 script.task.process_rules()
  30. import my_script as script PathFinder FileLoader def source_to_code(self, data, path,

    *, _optimize=-1):
 … tokens = translate(path)
 return compile(
 tokenize.untokenize(tokens), path, …
 )
  31. def translate(path):
 tokens = tokenize.generate_tokens(…)
 while tokens: ...
 if token_value

    in ('IN', ‘WITH'): ...
 yield from create_task() ...
 elif ...
 else:
 yield (tok_type, value)
  32. def translate(path):
 tokens = tokenize.generate_tokens(…)
 while tokens: ...
 if token_value

    in ('IN', ‘WITH'): ...
 yield from create_task() ...
 elif ...
 else:
 yield (tok_type, value) yield from create_task()
  33. def create_task():
 yield from [
 (tokenize.NAME, 'from'),
 (tokenize.NAME, 'model'),
 (tokenize.NAME,

    'import'),
 (tokenize.NAME, 'Task'),
 (tokenize.OP, ','),
 (tokenize.NAME, 'Rule'),
 (tokenize.NEWLINE, '\n'),
 (tokenize.NAME, 'task'),
 (tokenize.OP, '='),
 (tokenize.NAME, 'Task'),
 (tokenize.OP, '('),
 (tokenize.OP, ')'),
 (tokenize.NEWLINE, '\n')
 ]
  34. def create_task():
 yield from [
 (tokenize.NAME, 'from'),
 (tokenize.NAME, 'model'),
 (tokenize.NAME,

    'import'),
 (tokenize.NAME, 'Task'),
 (tokenize.OP, ','),
 (tokenize.NAME, 'Rule'),
 (tokenize.NEWLINE, '\n'),
 (tokenize.NAME, 'task'),
 (tokenize.OP, '='),
 (tokenize.NAME, 'Task'),
 (tokenize.OP, '('),
 (tokenize.OP, ')'),
 (tokenize.NEWLINE, '\n')
 ]
  35. DSL Ruby task = Task.new do with '*.\.mp3' inside './music'

    rule do Artist 'Metallica' end rule do Genre 'Rock' end end task.run()
  36. Внешние DSL

  37. Внешние DSL Нет базового языка Сами выбираем синтаксис Нет базового

    языка Необходимо разрабатывать анализаторы
  38. Свой язык WITH ".*\.mp3" IN "./music" SET Artist="Metallica" SET Genre="Rock"

  39. Python Lex-Yacc (PLY)

  40. ply.lex ply.yacc AST Run Code source

  41. ply.lex WITH ".*\.mp3" IN "./music" SET Artist="Metallica" SET Genre="Rock" Type

    Value WITH WITH IN IN SET SET EQUALS = VALUE \".*?\" ATTRIBUTE [A-Za-z][A-Za-z0-9]*
  42. ply.lex WITH ".*\.mp3" IN "./music" SET Artist="Metallica" SET Genre="Rock" SET

    EQUALS SET Artist="Metallica" ATTRIBUTE Artist VALUE "Metallica"
  43. ply.lex WITH Artist IN "Metallica" LexToken(WITH,'WITH',1,0) LexToken(ATTRIBUTE,'Artist',1,5) LexToken(IN,'IN',1,12) LexToken(VALUE,'"Metallica"',1,15)

  44. ply.yacc WITH ".*\.mp3" IN "./music" SET Artist="Metallica" SET Genre="Rock" rule

    : SET ATTRIBUTE EQUALS VALUE with : WITH VALUE in : IN VALUE rule_list : rule_list rule | rule task : with in rule_list | in rule_list
  45. PLY Генерируем AST

  46. ply.yacc WITH ".*\.mp3" IN "./music" SET Artist="Metallica" SET Genre="Rock" simple_token

    = namedtuple(
 'simple_token', ['Name', 'Value']
 )
 
 def p_rule(self, p):
 '''rule : SET ATTRIBUTE EQUALS VALUE'''
 p[0] = simple_token(Name='RULE', Value=(p[2], p[4]))
  47. ply.yacc WITH ".*\.mp3" IN "./music" SET Artist="Metallica" SET Genre="Rock" Task

    With In RuleList Rule Rule .*\.mp3 "./music" Artist Genre "Metallica" "Rock"
  48. PLY Заполняем модель

  49. ply.yacc WITH ".*\.mp3" IN "./music" SET Artist="Metallica" SET Genre="Rock" def

    p_rule(p):
 '''rule : SET ATTRIBUTE EQUALS VALUE'''
 p[0] = Rule(**{p[2]: p[4]})
  50. ply.yacc Task root_dir = "./music"
 file_mask = ".*\\.mp3"
 rules =

    [ ] Rule Artist = "Metallica" Rule Genre = "Rock" WITH ".*\.mp3" IN "./music" SET Artist="Metallica" SET Genre="Rock"
  51. PLY Гибкость Отладка Обработка ошибок Понятный код библиотеки Высокий порог

    входа Многословный
  52. funcparserlib

  53. funcparserlib WITH ".*\.mp3" IN "./music" SET Artist="Metallica" SET Genre="Rock" task

    = Task()
 
 root = keyword('In') + value_of('Value') >> set_root
 mask = keyword('With') + value_of('Value') >> set_mask
 rule = keyword('Set') + \
 value_of('Attribute') + \
 keyword('Equals') + \
 value_of('Value') \
 >> make_rule
 
 parser = maybe(mask) + root + many(rule)
 parser.parse(source)
  54. funcparserlib IN "./music" WITH ".*\.mp3" SET Artist="Metallica" SET Genre="Rock" get_value

    = lambda x: x.value
 value_of = lambda t: some(lambda x: x.type == t) >> get_value
 keyword = lambda s: skip(value_of(s))
 
 set_root = lambda value: task.set_root_dir(value[1:-1])
 set_mask = lambda value: task.set_mask(value[1:-1]) make_rule = lambda x: task.add_rule(Rule(**{x[0]: x[1]}))
  55. funcparserlib Компактный Гибкий Для любителей функционального программирования :) Многое приходится

    делать руками Для любителей функционального программирования :)
  56. pyparsing

  57. pyparsing WITH ".*\.mp3" IN "./music" SET Artist="Metallica" SET Genre="Rock" rule

    = (
 Keyword('SET') +
 Word(alphanums)('key') +
 '=' +
 QuotedString('"')('value')
 ).setParseAction(lambda r: {r.key: r.value}) >>> rule.parseString('SET Artist="Metallica"') {'Artist': 'Metallica'}
  58. pyparsing WITH ".*\.mp3" IN "./music" SET Artist="Metallica" SET Genre="Rock" {


    'mask': '.*\.mp3',
 'root_dir': './music',
 'rules': [
 {'Artist': 'Metallica'},
 {'Genre': 'Rock'}
 ]
 }
  59. pyparsing Понятная грамматика Базовые компоненты Свои компоненты Постобработка каждого элемента

    Документация Отладка
  60. Если сомневаетесь в необходимости DSL - попробуйте Начните с семантической

    модели, это ведь просто библиотека
  61. mi.0-0.im