Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

– Мартин Фаулер Предметно ориентированный язык — это язык программирования с ограниченными выразительными возможностями, ориентированный на некую конкретную предметную область

Slide 3

Slide 3 text

SQL SELECT * FROM Users WHERE Age>20

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

SQL REGEXP LaTeX E &=& mc^2\\ HTML PiterPy

Slide 7

Slide 7 text

SQL REGEXP LaTeX HTML PiterPy PonyORM select(p for p in Person if p.age > 20)

Slide 8

Slide 8 text

SQL REGEXP LaTeX HTML PonyORM WTForms class UsernameForm(Form):
 username = StringField('Username')

Slide 9

Slide 9 text

✤ SQL ✤ REGEXP ✤ TeX/LaTeX ✤ HTML Виды DSL DSL Внутренние Внешние ✤ PonyORM ✤ WTForm ✤ Django models

Slide 10

Slide 10 text

Высокая скорость разработки import re
 
 html_source = '''...'''
 
 links = re.findall(
 pattern=r'''.*?)\1>(?P.*?)''',
 string=html_source

Slide 11

Slide 11 text

Дополнительный уровень абстракции persons = select(p for p in Person if p.age > 20)[:]

Slide 12

Slide 12 text

Код могут писать "не-программисты" server { location / { proxy_pass http://localhost:8080/; } location ~ \.(gif|jpg|png)$ { root /data/images; } }

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

Перейдем к практике

Slide 20

Slide 20 text

Зачем нужна модель ✤ Хранит в себе всю бизнес-логику ✤ Позволяет использовать различные DSL ✤ Обеспечивает возможность тестирования

Slide 21

Slide 21 text

Семантическая модель

Slide 22

Slide 22 text

Внутренние DSL

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

Цепочки вызовов ✤ Все методы заполняют модель и возвращают объект ✤ Методы именуются исходя из смыслового контекста FileUpdater()\
 .path('\music')\
 .mask('.*metallica.*')\
 .set(Genre='Rock')\
 .set(Artist='Metallica'\
 .do()

Slide 25

Slide 25 text

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


Slide 26

Slide 26 text

Import hooks Очень мощный инструмент Вмешиваемся в работу интерпретатора Очень сложная отладка Непредсказуемые side- эффекты

Slide 27

Slide 27 text

Import hooks Не используйте это в реальном мире!

Slide 28

Slide 28 text

PathFinder FileLoader import requests def source_to_code(self, data, path, *, _optimize=-1):
 … source = self.get_source(path)
 return compile(
 source, path, …
 )

Slide 29

Slide 29 text

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()

Slide 30

Slide 30 text

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, …
 )

Slide 31

Slide 31 text

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)

Slide 32

Slide 32 text

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()

Slide 33

Slide 33 text

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')
 ]

Slide 34

Slide 34 text

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')
 ]

Slide 35

Slide 35 text

DSL Ruby task = Task.new do with '*.\.mp3' inside './music' rule do Artist 'Metallica' end rule do Genre 'Rock' end end task.run()

Slide 36

Slide 36 text

Внешние DSL

Slide 37

Slide 37 text

Внешние DSL Нет базового языка Сами выбираем синтаксис Нет базового языка Необходимо разрабатывать анализаторы

Slide 38

Slide 38 text

Свой язык WITH ".*\.mp3" IN "./music" SET Artist="Metallica" SET Genre="Rock"

Slide 39

Slide 39 text

Python Lex-Yacc (PLY)

Slide 40

Slide 40 text

ply.lex ply.yacc AST Run Code source

Slide 41

Slide 41 text

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]*

Slide 42

Slide 42 text

ply.lex WITH ".*\.mp3" IN "./music" SET Artist="Metallica" SET Genre="Rock" SET EQUALS SET Artist="Metallica" ATTRIBUTE Artist VALUE "Metallica"

Slide 43

Slide 43 text

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)

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

PLY Генерируем AST

Slide 46

Slide 46 text

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]))

Slide 47

Slide 47 text

ply.yacc WITH ".*\.mp3" IN "./music" SET Artist="Metallica" SET Genre="Rock" Task With In RuleList Rule Rule .*\.mp3 "./music" Artist Genre "Metallica" "Rock"

Slide 48

Slide 48 text

PLY Заполняем модель

Slide 49

Slide 49 text

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]})

Slide 50

Slide 50 text

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"

Slide 51

Slide 51 text

PLY Гибкость Отладка Обработка ошибок Понятный код библиотеки Высокий порог входа Многословный

Slide 52

Slide 52 text

funcparserlib

Slide 53

Slide 53 text

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)

Slide 54

Slide 54 text

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]}))

Slide 55

Slide 55 text

funcparserlib Компактный Гибкий Для любителей функционального программирования :) Многое приходится делать руками Для любителей функционального программирования :)

Slide 56

Slide 56 text

pyparsing

Slide 57

Slide 57 text

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'}

Slide 58

Slide 58 text

pyparsing WITH ".*\.mp3" IN "./music" SET Artist="Metallica" SET Genre="Rock" {
 'mask': '.*\.mp3',
 'root_dir': './music',
 'rules': [
 {'Artist': 'Metallica'},
 {'Genre': 'Rock'}
 ]
 }

Slide 59

Slide 59 text

pyparsing Понятная грамматика Базовые компоненты Свои компоненты Постобработка каждого элемента Документация Отладка

Slide 60

Slide 60 text

Если сомневаетесь в необходимости DSL - попробуйте Начните с семантической модели, это ведь просто библиотека

Slide 61

Slide 61 text

mi.0-0.im