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

Вы, наверняка, сталкивались с ситуациями, когда стандартные языки программирования оказывались не совсем подходящими для вашей конкретной задачи. Именно здесь на сцену выходит понятие Domain-Specific Language, или DSL. Этот инструмент позволяет создать специализированный язык, точно соответствующий потребностям вашей области.

Давайте честно признаемся, иногда обычные языки программирования могут быть немного громоздкими. Создавая DSL, вы фокусируетесь исключительно на нужных вам аспектах, избегая лишней мороки. Это как пошаговый рецепт в кулинарии: вы не будете месить тесто, если делаете суп. Таким образом, ваш код становится чище, понятнее и эффективнее.

Выбор языка и подхода

Объяснение выбора Python как базового языка

Итак, почему Python? Этот язык программирования, полюбившийся миллионам, выбран не случайно. Он как палитра художника, позволяет нам создавать шедевры DSL с невероятной легкостью. Python обладает чистым и выразительным синтаксисом, который, словно кисть художника, позволяет нам создавать красивые и понятные конструкции нового языка.

Заглянув в прошлое, мы увидим, что Python изначально был спроектирован для удобочитаемости и удовольствия от написания кода. Именно эти черты делают Python идеальным кандидатом для создания DSL. Мы можем выразить наши идеи в простых и понятных конструкциях.

Сравнение встроенных и внешних DSL

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

Встроенный DSL живет внутри хост-языка, в нашем случае - Python. Он позволяет нам вкладывать специфические для области задачи конструкции прямо в код на Python. Это как пение птиц в лесу, вписывающееся в общий хор природы. Внешний DSL - это отдельный язык, который мы создаем независимо от Python. Он как сольное исполнение, подчеркивающее глубину и красоту наших идей.

Определение целей и требований к создаваемому языку

Теперь, когда мы выбрали Python и решили, какой подход используем, пришло время обозначить цели и требования будущего произведения.

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

Проектирование DSL: Как Создать Свой Мир в Python Коде

Определение синтаксиса

Синтаксис нашего DSL - это стиль, который будет описывать наши идеи.

Давайте рассмотрим пример. Предположим, что вы создаете DSL для управления роботами. Ваши ключевые слова могут быть move, rotate, scan, а операторы - например, degrees, meters. Теперь вы можете писать что-то вроде:

robot.move(2.5, meters).rotate(90, degrees).scan()

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

Выбор структур данных

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

Давайте вернемся к нашему примеру с роботами. Вы можете использовать классы и объекты для представления роботов и их действий:

class Robot:
    def __init__(self):
        self.actions = []

    def move(self, distance, unit):
        self.actions.append(f"Move {distance} {unit}")
        return self

    def rotate(self, angle, unit):
        self.actions.append(f"Rotate {angle} {unit}")
        return self

    def scan(self):
        self.actions.append("Scan")
        return self

Таким образом, вы можете строить последовательность действий в вашем DSL:

robot = Robot()
robot.move(2.5, meters).rotate(90, degrees).scan()

Разработка семантики

Семантика - это то, как интерпретируются действия вашего DSL.

Продолжая пример, давайте добавим семантику к действиям робота:

class Robot:
    # ... (предыдущий код)

    def execute(self):
        for action in self.actions:
            print(f"Executing: {action}")

robot = Robot()
robot.move(2.5, meters).rotate(90, degrees).scan().execute()

Взаимодействие с существующими библиотеками и кодом на Python

И, наконец, приходим к тому, как объединить ваш DSL с уже существующим кодом и библиотеками на Python. Ваш DSL может стать интегральной частью проекта.

Для этого давайте рассмотрим, как можно взаимодействовать с библиотекой для работы с роботами:

class Robot:
    # ... (предыдущий код)

    def connect_to_robot(self, real_robot):
        self.real_robot = real_robot

    def execute(self):
        for action in self.actions:
            print(f"Executing: {action}")
            # Пример вызова метода библиотеки
            if "Move" in action:
                self.real_robot.move()
            # ... другие действия

real_robot = RealRobot()  # Подразумевается, что у нас есть класс для реального робота
robot = Robot()
robot.connect_to_robot(real_robot)
robot.move(2.5, meters).rotate(90, degrees).scan().execute()

Имплементация встроенного языка:

Использование библиотеки Python для создания парсера

Парсер - это как режиссер, который расставляет актеров на сцене в правильном порядке. Мы можем использовать библиотеки Python для создания парсера и превратить наши строки кода в структуры данных.

Представьте, что у нас есть DSL для математических выражений:

expr = "add(5, multiply(3, 2))"

Используя библиотеку pyparsing, мы можем создать парсер:

from pyparsing import Word, nums, Forward, Group, Suppress

integer = Word(nums).setParseAction(lambda tokens: int(tokens[0]))
ident = Word(alphas)

func_call = Forward()
atom = integer | ident + Suppress("(") + func_call + Suppress(")")
func_call <<= ident + Suppress("(") + Group(atom[...] + Suppress(",").leaveWhitespace()) + atom + Suppress(")")

parsed_expr = func_call.parseString(expr, parseAll=True)
print(parsed_expr)

Создание абстрактного синтаксического дерева (AST)

А теперь наш парсер создает абстрактное синтаксическое дерево (AST). AST позволяет нам представить код в виде древовидной структуры, что делает его более удобным для обработки.

Давайте продолжим работу с нашим примером:

class Node:
    def __init__(self, value):
        self.value = value
        self.children = []

for token in parsed_expr:
    if isinstance(token, str):
        node = Node(token)
        parsed_expr.insert(0, node)
    else:
        node.children.append(token)

Теперь AST содержит информацию о структуре нашего кода, как актеры и их реплики.

Мы можем превратить наше DSL в настоящий Python код:

def evaluate(node):
    if isinstance(node.value, int):
        return node.value
    elif node.value == "add":
        return sum(evaluate(child) for child in node.children)
    elif node.value == "multiply":
        result = 1
        for child in node.children:
            result *= evaluate(child)
        return result

print(evaluate(parsed_expr[0]))

Код DSL был преобразован в исполняемый Python код.

Обеспечение читаемости и проверки Кода

Добавление комментариев и документации для DSL

Давайте представим, что ваш код - это книга, а комментарии и документация - это как разговор с автором. Хорошо оформленные комментарии делают код более понятным и доступным. Они помогут разработчикам, использующим ваш DSL, быстрее разобраться в его функциональности.

Продолжая историю с роботами:

class Robot:
    # ... (предыдущий код)

    def move(self, distance, unit):
        """Move the robot by a specified distance."""
        # ... (код функции)

    def rotate(self, angle, unit):
        """Rotate the robot by a specified angle."""
        # ... (код функции)

    def scan(self):
        """Perform a scan operation."""
        # ... (код функции)

Добавление такой документации позволит пользователям вашего DSL легко понять, что делает каждая функция, и как правильно ими пользоваться.

Разработка модульных тестов для языка

Перейдем к тестированию вашего DSL. Модульные тесты - это как репетиции перед важным выступлением. Они обеспечивают надежность вашего кода и уверенность в его работе.

Пример:

import unittest

class TestRobotDSL(unittest.TestCase):
    def test_move(self):
        robot = Robot()
        robot.move(2.5, meters)
        self.assertEqual(robot.execute(), "Move 2.5 meters")

    def test_rotate(self):
        robot = Robot()
        robot.rotate(90, degrees)
        self.assertEqual(robot.execute(), "Rotate 90 degrees")

if __name__ == '__main__':
    unittest.main()

Такие тесты помогут быстро выявить ошибки и убедиться, что ваш DSL работает так, как задумано.

Использование линтеров и статического анализа для обеспечения качества кода

Линтеры и статический анализ позволяет обнаруживать потенциальные проблемы в коде и помогают соблюсти стандарты оформления.

Используем pylint:

pip install pylint

Затем запускаем линтер:

pylint your_dsl_module.py

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

Пример создания языка для Хабра

Давайте взглянем, как можно создать свой язык для Хабра .

Шаг 1: Определение Синтаксиса и Ключевых Конструкций

Давайте начнем с определения, как пользователи будут использовать наш DSL для Хабра. Допустим, мы хотим, чтобы пользователи могли легко вставлять ссылки на статьи и цитировать интересные моменты. Давайте создадим ключевые слова link и quote:

link("URL")
quote("Текст цитаты")

Шаг 2: Разработка Синтаксиса

Теперь создадим парсер для DSL. Используем библиотеку pyparsing, чтобы превратить наши строки в объекты Python:

from pyparsing import Word, alphas, Suppress, QuotedString

link_keyword = Suppress("link")
quote_keyword = Suppress("quote")
url = QuotedString('"')
text = QuotedString('"', multiline=True)

link_parser = link_keyword + "(" + url + ")"
quote_parser = quote_keyword + "(" + text + ")"

Шаг 3: Создание Абстрактного Синтаксического Дерева (AST)

Теперь создадим классы для представления объектов нашего DSL:

class LinkNode:
    def __init__(self, url):
        self.url = url

class QuoteNode:
    def __init__(self, text):
        self.text = text

Шаг 4: Преобразование в Python Код

Давайте добавим методы __str__ к нашим классам, чтобы они могли преобразовывать себя в строку Python кода:

class LinkNode:
    # ... (предыдущий код)

    def __str__(self):
        return f'link("{self.url}")'

class QuoteNode:
    # ... (предыдущий код)

    def __str__(self):
        return f'quote("{self.text}")'

Шаг 5: Пример Использования

Итак, наш DSL готов! Теперь давайте взглянем, как он будет выглядеть в действии:

content = [
    link("https://habr.com"),
    quote("Создание DSL на основе Python"),
    link("https://habr.com/article/12345"),
    quote("OTUS")
]

for item in content:
    print(item)

Шаг 6: Дополнительные Идеи

Мы создали DSL для добавления ссылок и цитат, но почему бы не добавить еще функциональности? Например, вы можете создать ключевые слова для создания списков, подчеркивания текста и даже вставки изображений. Cоздадим ключевые слова list, underline и image:

codelist("Элемент 1", "Элемент 2", "Элемент 3")
underline("Выделенный текст")
image("URL_изображения", caption="Подпись к изображению")

Управление ошибками и расширение языка

Обработка ошибок и исключений в DSL

Мы не можем избежать ошибок.

Давайте внедрим эту идею в DSL, в примере про Хабр. Представим, что у нас есть функция quote, и пользователь случайно забыл закрыть кавычку:

quote("Забыл закрыть кавычку)

Чтобы предостеречь пользователя, давайте добавим проверку и выбросим исключение:

class QuoteNode:
    def __init__(self, text, author=None):
        if '"' not in text:
            raise ValueError("Текст цитаты должен быть заключен в кавычки")
        self.text = text
        self.author = author

Теперь, если пользователь допустит ошибку, он сразу получит человекочитаемое сообщение о том, что пошло не так.

Возможности для расширения функциональности языка

Наш DSL уже позволяет делать некоторые вещи на Хабре. Но давайте сделаем его еще более расширенным. К примеру добавим: code для вставки программного кода и image для изображений:

code("Python", "print('Hello, DSL!')")
image("https://example.com/image.jpg", caption="Красивое изображение")

Добавление новых функций позволяет пользователям создавать разнообразный контент прямо из нашего DSL.

Поддержка новых фич без нарушения существующей функциональности

Пока мы добавляем новые фичи, важно не нарушить работу уже существующего функционала. Для этого давайте будем использовать концепцию наследования.

class ImageNode:
    def __init__(self, url, caption=None):
        self.url = url
        self.caption = caption

class CodeNode:
    def __init__(self, language, code):
        self.language = language
        self.code = code

Таким образом, мы добавляем новые классы для новых функций, не затрагивая работу существующих.

Заключение

Создание DSL - это как написание симфонии, где вы являетесь композитором. Вы определяете ноты, решаете, как они будут звучать, и создаете гармонию. Ваш DSL не только делает код более читаемым и удобным, но и вдохновляет на новые творческие высоты.

Продолжайте исследовать, улучшать и расширять свой DSL. Добавляйте новые функции, экспериментируйте с новыми идеями и делайте свой вклад в свое развитие. Ваш DSL - это ключ к бесконечным возможностям и творческому росту.

На открытых уроках, которые проводят эксперты из OTUS вы сможете получить еще больше практических разборов различных инструментов и почерпнуть много полезной информации. Заглядывайте в календарь и регистрируйтесь на интересующие вас мероприятия. Все открытые уроки проводятся бесплатно.

Комментарии (3)


  1. KivyMD
    31.08.2023 15:45
    +7

    Я так и не понял, причем тут DSL?


    1. forthuse
      31.08.2023 15:45
      -1

      Тоже не понял! :)


      DSL на Python — это примерно как создать и описать проект "Шарлатанского" языка Quackery и порешать на нём ещё и задачи Quackery на rosettacode.org


      P.S. Может chatGPT так понимает концепцию DSL в рамках Python применения? :)


  1. vagon333
    31.08.2023 15:45

    Посмотрите ANTLR.