Введение: когда универсальность — враг
Представьте, что вы пишете код для конфигурации сети, парсинга логов или описания бизнес-правил. Каждый раз — однотипные конструкции, куча скобок, вложенные условные операторы и мучительное чтение чужого кода. Знакомо?
В таких ситуациях на помощь приходят предметно-ориентированные языки (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": "Иван", ...], ...]
Этот код — уже полноценный инструмент. Вы можете расширять его