Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

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

653 views

Published on

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.

Published in: Technology
  • Be the first to comment

  • Be the first to like this

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

  1. 1. DSL в Python. Цыганов Иван Positive Technologies Как и зачем?
  2. 2. – Мартин Фаулер Предметно ориентированный язык — это язык программирования с ограниченными выразительными возможностями, ориентированный на некую конкретную предметную область
  3. 3. SQL SELECT * FROM Users WHERE Age>20
  4. 4. SQL SELECT * FROM Users WHERE Are>20 REGEXP [A-Z][A-Za-z0-9]*
  5. 5. SQL REGEXP [A-Z]w+ LaTeX E &=& mc^2
  6. 6. SQL REGEXP LaTeX E &=& mc^2 HTML <a href='http://piterpy.ru'>PiterPy</a>
  7. 7. SQL REGEXP LaTeX HTML <a href='http://piterpy.ru'>PiterPy</a> PonyORM select(p for p in Person if p.age > 20)
  8. 8. SQL REGEXP LaTeX HTML PonyORM WTForms class UsernameForm(Form):
 username = StringField('Username')
  9. 9. ✤ SQL ✤ REGEXP ✤ TeX/LaTeX ✤ HTML Виды DSL DSL ВнутренниеВнешние ✤ PonyORM ✤ WTForm ✤ Django models
  10. 10. Высокая скорость разработки import re
 
 html_source = '''...'''
 
 links = re.findall(
 pattern=r'''<a href=("|')(?P<URL>.*?)1>(?P<Text>.*?)</a>''',
 string=html_source
  11. 11. Дополнительный уровень абстракции persons = select(p for p in Person if p.age > 20)[:]
  12. 12. Код могут писать "не-программисты" server { location / { proxy_pass http://localhost:8080/; } location ~ .(gif|jpg|png)$ { root /data/images; } }
  13. 13. Проблемы и решения Высокая стоимость разработки DSL
  14. 14. Проблемы и решения Достаточно один раз разобраться Высокая стоимость разработки DSL
  15. 15. Проблемы и решения Достаточно один раз разобраться Высокая стоимость разработки DSL Нет специалистов со знанием языка
  16. 16. Проблемы и решения Достаточно один раз разобраться Высокая стоимость разработки DSL Язык должен иметь ограниченные возможности Нет специалистов со знанием языка
  17. 17. Проблемы и решения Достаточно один раз разобраться Высокая стоимость разработки DSL Язык должен иметь ограниченные возможности Нет специалистов со знанием языка Работает не для всех задач
  18. 18. Проблемы и решения Достаточно один раз разобраться Высокая стоимость разработки DSL Язык должен иметь ограниченные возможности Нет специалистов со знанием языка Стоит попробовать, что бы понять Работает не для всех задач
  19. 19. Перейдем к практике
  20. 20. Зачем нужна модель ✤ Хранит в себе всю бизнес-логику ✤ Позволяет использовать различные DSL ✤ Обеспечивает возможность тестирования
  21. 21. Семантическая модель
  22. 22. Внутренние DSL
  23. 23. Внутренние DSL Все возможности базового языка Привычный синтаксис Легко работать Ограничен базовым языком
  24. 24. Цепочки вызовов ✤ Все методы заполняют модель и возвращают объект ✤ Методы именуются исходя из смыслового контекста FileUpdater()
 .path('music')
 .mask('.*metallica.*')
 .set(Genre='Rock')
 .set(Artist='Metallica'
 .do()
  25. 25. Вложенные функции ✤ Для заполнения модели вызываются функции ✤ Функции именуются исходя из смыслового контекста update(
 settings(
 path('./music'),
 mask('.*.mp3')
 ),
 set(Artist='Metallica'),
 set(Genre='Rock')
 )

  26. 26. Import hooks Очень мощный инструмент Вмешиваемся в работу интерпретатора Очень сложная отладка Непредсказуемые side- эффекты
  27. 27. Import hooks Не используйте это в реальном мире!
  28. 28. PathFinder FileLoader import requests def source_to_code(self, data, path, *, _optimize=-1):
 … source = self.get_source(path)
 return compile(
 source, path, …
 )
  29. 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. 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. 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. 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. 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. 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. 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. 36. Внешние DSL
  37. 37. Внешние DSL Нет базового языка Сами выбираем синтаксис Нет базового языка Необходимо разрабатывать анализаторы
  38. 38. Свой язык WITH ".*.mp3" IN "./music" SET Artist="Metallica" SET Genre="Rock"
  39. 39. Python Lex-Yacc (PLY)
  40. 40. ply.lex ply.yacc ASTRunCode source
  41. 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. 42. ply.lex WITH ".*.mp3" IN "./music" SET Artist="Metallica" SET Genre="Rock" SET EQUALS SET Artist="Metallica" ATTRIBUTE Artist VALUE "Metallica"
  43. 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. 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. 45. PLY Генерируем AST
  46. 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. 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. 48. PLY Заполняем модель
  49. 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. 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. 51. PLY Гибкость Отладка Обработка ошибок Понятный код библиотеки Высокий порог входа Многословный
  52. 52. funcparserlib
  53. 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. 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. 55. funcparserlib Компактный Гибкий Для любителей функционального программирования :) Многое приходится делать руками Для любителей функционального программирования :)
  56. 56. pyparsing
  57. 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. 58. pyparsing WITH ".*.mp3" IN "./music" SET Artist="Metallica" SET Genre="Rock" {
 'mask': '.*.mp3',
 'root_dir': './music',
 'rules': [
 {'Artist': 'Metallica'},
 {'Genre': 'Rock'}
 ]
 }
  59. 59. pyparsing Понятная грамматика Базовые компоненты Свои компоненты Постобработка каждого элемента Документация Отладка
  60. 60. Если сомневаетесь в необходимости DSL - попробуйте Начните с семантической модели, это ведь просто библиотека
  61. 61. mi.0-0.im

×