Создаём свой язык программирования: DSL на JavaScript

Онлайн-тренажер Python для начинающих

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

Начать курс

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

Представьте: вы пишете код, который читается как обычный английский текст. Никаких лишних скобок, никаких for и if, только чистые, понятные инструкции. Звучит как фантастика? А вот и нет. Это DSL (Domain-Specific Language) — предметно-ориентированный язык, созданный под конкретную задачу.

На PythonLib мы часто говорим о том, как сделать код чище и выразительнее. Сегодня мы зайдём с неожиданной стороны: возьмём JavaScript и превратим его в инструмент для создания собственных мини-языков. Зачем? Чтобы ваши конфигурации перестали быть «кашей», а бизнес-логика зазвучала как музыка.

Готовы стать лингвистом в мире программирования? Поехали!

DSL: что это и зачем оно вам?

DSL (предметно-ориентированный язык) — это язык программирования (или его синтаксическое расширение), созданный для решения задач из одной конкретной области. В отличие от универсальных языков (C++, Python, Java), DSL не пытается объять необъятное. Его задача — быть максимально выразительным в своей нише.

Примеры из жизни:

  • SQL — язык для работы с базами данных.
  • HTML — язык разметки веб-страниц.
  • Regex — язык для поиска по шаблону.
  • Makefile — язык описания сборки проектов.

А теперь представьте, что вы можете создать такой же удобный язык для своей задачи: для описания бизнес-правил, для конфигурации ботов, для построения сложных запросов к API. Создание DSL на JavaScript — это суперсила, которая делает код самодокументируемым и снижает порог входа для новых разработчиков.

Внутренние и внешние DSL: два пути к одной цели

Внутренний DSL (Embedded DSL)

Это надстройка над существующим языком. Вы используете синтаксис JavaScript, но строите из него «кирпичики» нового языка. Самый популярный пример — Fluent API (цепочки методов).

Плюсы: не нужно писать парсер, используете все возможности хост-языка (отладка, типы, инструменты).
Минусы: ограничены синтаксисом JavaScript (никаких новых операторов).

Внешний DSL

Вы придумываете свой синтаксис с нуля, а затем пишете парсер (или компилятор), который превращает его в JavaScript.

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

Для большинства проектов DSL на JavaScript начинается с внутреннего подхода — он проще, и его можно внедрить уже сегодня.

Пишем первый DSL: от конфигурации к поэзии

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

const routes = [
    { path: '/users', handler: usersHandler, method: 'GET' },
    { path: '/users/:id', handler: userById, method: 'GET' },
    { path: '/users', handler: createUser, method: 'POST' }
];

Мы хотим писать так:

const router = new Router();
router
    .get('/users', usersHandler)
    .get('/users/:id', userById)
    .post('/users', createUser);

Уже лучше, правда? Но это только начало. Настоящий DSL должен быть ещё выразительнее.

Шаг 1: Fluent API (Цепочки методов)

Самый простой способ сделать предметно-ориентированный язык — вернуть this из каждого метода.

class Router {
    constructor() {
        this.routes = [];
    }

    get(path, handler) {
        this.routes.push({ path, handler, method: 'GET' });
        return this; // ключ к цепочке
    }

    post(path, handler) {
        this.routes.push({ path, handler, method: 'POST' });
        return this;
    }

    delete(path, handler) {
        this.routes.push({ path, handler, method: 'DELETE' });
        return this;
    }

    // ... и так далее
}

Теперь мы можем писать:

const appRouter = new Router()
    .get('/', homePage)
    .get('/about', aboutPage)
    .post('/contact', contactForm);

Это уже DSL на JavaScript — простой, но эффективный.

Шаг 2: Добавляем контекст и вложенность

Настоящая магия начинается, когда мы добавляем вложенные блоки. Представьте, что мы хотим описывать конфигурацию игры:

const gameConfig = new GameConfig()
    .level('forest')
        .enemy('goblin', { hp: 10, damage: 2 })
        .enemy('orc', { hp: 20, damage: 5 })
        .treasure('gold', 100)
    .level('dungeon')
        .enemy('skeleton', { hp: 15, damage: 3 })
        .treasure('sword', 1);

Как это реализовать? Используем контекстный менеджер:

class GameConfig {
    constructor() {
        this.levels = {};
        this.currentLevel = null;
    }

    level(name) {
        if (this.currentLevel) {
            this.levels[this.currentLevel.name] = this.currentLevel;
        }
        this.currentLevel = { name, enemies: [], treasures: [] };
        return this;
    }

    enemy(type, stats) {
        if (this.currentLevel) {
            this.currentLevel.enemies.push({ type, ...stats });
        }
        return this;
    }

    treasure(item, quantity) {
        if (this.currentLevel) {
            this.currentLevel.treasures.push({ item, quantity });
        }
        return this;
    }

    build() {
        if (this.currentLevel) {
            this.levels[this.currentLevel.name] = this.currentLevel;
        }
        return this.levels;
    }
}

// Использование:
const config = new GameConfig()
    .level('forest')
        .enemy('goblin', { hp: 10 })
        .treasure('gold', 100)
    .level('dungeon')
        .enemy('skeleton', { hp: 15 })
    .build();

console.log(config);
// {
//   forest: { name: 'forest', enemies: [ { type: 'goblin', hp: 10 } ], treasures: [ { item: 'gold', quantity: 100 } ] },
//   dungeon: { name: 'dungeon', enemies: [ { type: 'skeleton', hp: 15 } ], treasures: [] }
// }

Обратите внимание: мы использовали отступы для наглядности, но в реальном коде это работает благодаря возврату this. Этот паттерн называется Builder Pattern и является основой многих внутренних DSL.

Идём дальше: создаём внешний DSL

Иногда возможностей JavaScript не хватает. Хочется писать не цепочки, а настоящие предложения. Например, для описания бизнес-правил:

RULE "                

Блоги

Рекомендаций по книгам