DSL на Swift: как создать свой язык программирования за вечер

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

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

Начать курс

Введение: когда универсальность — враг

Представьте, что вы пишете код для конфигурации сети, парсинга логов или описания бизнес-правил. Каждый раз — однотипные конструкции, куча скобок, вложенные условные операторы и мучительное чтение чужого кода. Знакомо?

В таких ситуациях на помощь приходят предметно-ориентированные языки (DSL). Это не магия и не изобретение велосипеда — это способ сделать код настолько читаемым, что он сам себя документирует. В этой статье мы разберём, как создать собственный DSL на Swift — мощный, элегантный и при этом не требующий компилятора.

DSL на Swift — это не просто модный тренд iOS-разработки. Это инструмент, который используют в SwiftUI, Combine и даже в библиотеках для бэкенда. Вы научитесь проектировать языки, которые решают конкретные задачи: от парсинга данных до описания UI. Поехали!

Что такое DSL и зачем он нужен разработчику?

Domain-Specific Language — это язык, заточенный под одну узкую область. В отличие от общего языка (вроде Swift или Python), DSL говорит на языке вашей предметной области. Например, вместо:

let config = NetworkConfig()
config.host = "api.example.com"
config.port = 443
config.timeout = 30

Вы пишете:

let config = networkConfig {
    host("api.example.com")
    port(443)
    timeout(30)
}

Второй вариант читается как инструкция на человеческом языке. Это и есть цель DSL — снизить когнитивную нагрузку и сделать код самодокументируемым.

В Swift для создания DSL чаще всего используют:

  • Result Builders — новая фича Swift 5.4+, позволяющая строить вложенные структуры через декларативный синтаксис.
  • Функциональные цепочки — классический builder pattern.
  • Операторы — перегрузка операторов для создания fluent-интерфейсов.

Но давайте сразу к делу. Мы создадим DSL для парсинга CSV-файлов — задача, которая мучает каждого разработчика хотя бы раз в жизни.

Пример 1: Простой DSL для конфигурации сети

Начнём с базового примера, который покажет механику. Допустим, нам нужно описывать HTTP-запросы. Вместо кучи параметров — лаконичный DSL.

// Определяем структуру запроса
struct HTTPRequest {
    var method: String = "GET"
    var path: String = "/"
    var headers: [String: String] = [:]
    var body: Data? = nil
}

// Создаём DSL через Result Builder
@resultBuilder
struct RequestBuilder {
    static func buildBlock(_ components: HTTPRequestModifier...) -> HTTPRequest {
        var request = HTTPRequest()
        for mod in components {
            mod.apply(&request)
        }
        return request
    }
}

// Модификаторы запроса
protocol HTTPRequestModifier {
    func apply(_ request: inout HTTPRequest)
}

struct MethodModifier: HTTPRequestModifier {
    let method: String
    func apply(_ request: inout HTTPRequest) {
        request.method = method
    }
}

struct PathModifier: HTTPRequestModifier {
    let path: String
    func apply(_ request: inout HTTPRequest) {
        request.path = path
    }
}

// Вспомогательные функции
func method(_ m: String) -> HTTPRequestModifier { MethodModifier(method: m) }
func path(_ p: String) -> HTTPRequestModifier { PathModifier(path: p) }

// Использование
@RequestBuilder
func buildGetRequest() -> HTTPRequest {
    method("GET")
    path("/users")
}

let request = buildGetRequest()
print(request.method, request.path) // GET /users

Этот код — основа любого DSL на Swift. @resultBuilder собирает все выражения внутри блока и преобразует их в единую структуру. Важно: каждый элемент — это модификатор, который меняет состояние.

Теперь усложним задачу: добавим вложенность и условия.

Пример 2: DSL для парсинга CSV (реальная задача)

CSV-файлы — это боль. Разные разделители, заголовки, экранированные кавычки. Напишем DSL, который превращает парсинг в удовольствие.

// Описываем схему CSV
struct CSVSchema {
    var hasHeader: Bool = true
    var delimiter: Character = ","
    var columns: [String: CSVColumn] = [:]
}

struct CSVColumn {
    let name: String
    let type: CSVType
    var required: Bool = false
}

enum CSVType {
    case string, int, double, date
}

// DSL через Result Builder
@resultBuilder
struct CSVSchemaBuilder {
    static func buildBlock(_ components: CSVSchemaComponent...) -> CSVSchema {
        var schema = CSVSchema()
        for comp in components {
            comp.apply(&schema)
        }
        return schema
    }
}

protocol CSVSchemaComponent {
    func apply(_ schema: inout CSVSchema)
}

struct DelimiterComponent: CSVSchemaComponent {
    let char: Character
    func apply(_ schema: inout CSVSchema) {
        schema.delimiter = char
    }
}

struct ColumnComponent: CSVSchemaComponent {
    let name: String
    let type: CSVType
    let required: Bool
    func apply(_ schema: inout CSVSchema) {
        schema.columns[name] = CSVColumn(name: name, type: type, required: required)
    }
}

// Вспомогательные функции
func delimiter(_ c: Character) -> CSVSchemaComponent { DelimiterComponent(char: c) }
func column(_ name: String, type: CSVType, required: Bool = false) -> CSVSchemaComponent {
    ColumnComponent(name: name, type: type, required: required)
}

// Использование
@CSVSchemaBuilder
func userCSVSchema() -> CSVSchema {
    delimiter(";")
    column("id", type: .int, required: true)
    column("name", type: .string)
    column("email", type: .string, required: true)
    column("age", type: .int)
}

let schema = userCSVSchema()
print(schema.columns.count) // 4

Теперь добавим парсер, который использует эту схему:

struct CSVParser {
    let schema: CSVSchema
    
    func parse(_ raw: String) -> [[String: Any]] {
        let lines = raw.split(separator: "\n").map(String.init)
        let dataLines = schema.hasHeader ? Array(lines.dropFirst()) : lines
        let headers = schema.hasHeader ? lines.first?.split(separator: schema.delimiter).map(String.init) : nil
        
        return dataLines.map { line in
            let values = line.split(separator: schema.delimiter).map(String.init)
            var row: [String: Any] = [:]
            for (index, value) in values.enumerated() {
                let key = headers?[index] ?? "col\(index)"
                if let column = schema.columns[key] {
                    let parsed = parseValue(value, as: column.type)
                    row[key] = parsed
                }
            }
            return row
        }
    }
    
    private func parseValue(_ value: String, as type: CSVType) -> Any {
        switch type {
        case .int: return Int(value) ?? 0
        case .double: return Double(value) ?? 0.0
        case .string: return value
        case .date: return value // в реальности — DateFormatter
        }
    }
}

// Пример использования
let csvData = """
id;name;email;age
1;Иван;ivan@test.com;30
2;Мария;maria@test.com;25
"""

let parser = CSVParser(schema: userCSVSchema())
let result = parser.parse(csvData)
print(result) // [["id": 1, "name": "Иван", ...], ...]

Этот код — уже полноценный инструмент. Вы можете расширять его

Блоги

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