Поскольку это довольно строгий статически компилируемый язык, с первого взгляда может показаться, что Swift мало чего может предложить в плане кастомизации синтаксиса, но на самом деле это далеко не так. Благодаря таким фичам, как настраиваемые и перегруженные операторы, key paths, function/result builders и т. д., у нас есть множество возможностей для настройки синтаксиса Swift под конкретные сценарии использования.

Конечно, можно определенно утверждать, что к любому виду кастомизации синтаксиса следует подходить с большой осторожностью, поскольку нестандартный синтаксис может легко стать источником путаницы, если мы не будем достаточно осторожны. Но в определенных ситуациях этот компромисс может вполне того стоить и может позволить нам создавать что-то вроде «микро-DSL», которые на самом деле могут помочь нам сделать наш код более понятным, а не наоборот.

Инвертированные логические key paths

Чтобы рассмотреть один из таких случаев, предположим, что мы работаем над приложением для управления, фильтрации и сортировки статей, которое имеет следующую модель данных (Article):

struct Article {
    var title: String
    var body: String
    var category: Category
    var isRead: Bool
    ...
    
}

Теперь предположим, что очень распространенной задачей в нашей кодовой базе является фильтрация различных коллекций, каждая из которых содержит экземпляры указанной выше модели. Один из способов сделать это - использовать тот факт, что любой key path литерал Swift может быть автоматически преобразован в функцию, что позволяет нам использовать следующий компактный синтаксис при фильтрации по любому логическому свойству, например в данном случае isRead:

let articles: [Article] = ...
let readArticles = articles.filter(\.isRead)

Это действительно здорово, однако приведенный выше синтаксис можно использовать только тогда, когда мы хотим сравнить с true - это означает, что если бы мы хотели создать аналогично отфильтрованный массив, содержащий все непрочитанные статьи, нам пришлось бы использовать вместо этого замыкание (или передавать функцию):

let unreadArticles = articles.filter { !$0.isRead }

Это, конечно, не очень большая проблема, но если описанные выше операции выполняются во многих разных местах нашей кодовой базы, тогда мы можем начать спрашивать себя: «А было бы здорово, если бы мы могли также использовать тот же красивый key path синтаксис для инвертированных логических значений? "

Здесь на помощь приходит концепция кастомизации синтаксиса. Реализуя следующую префиксную функцию, мы фактически можем создать небольшую настройку, которая позволит нам использовать key path независимо от того, сравниваем ли мы с true или false:

prefix func !<T>(keyPath: KeyPath<T, Bool>) -> (T) -> Bool {
    return { !$0[keyPath: keyPath] }
}

Вышеупомянутое, по сути, является перегрузкой встроенного префиксного оператора !, который позволяет применить этот оператор к любому Bool key path, чтобы превратить его в функцию, которая инвертирует (или переворачивает) его значение, что, в свою очередь, теперь позволяет нам обработать наш массив unreadArticles следующим образом:

let unreadArticles = articles.filter(!\.isRead)

Это довольно круто и не делает наш код непонятнее, учитывая, что мы используем оператор ! таким образом, который соответствует тому, как он используется по умолчанию - для отрицания логического выражения.

Сравнение на основе key paths

 Мы можем пойти еще дальше и также сделать возможным использование key paths для формирования фильтрующих запросов, которые сравнивают данное свойство с любым видом значения Equatable. Это стало бы полезным, если бы мы, например, захотели иметь возможность отфильтровать массив Equatable по каждой категории (category) статьи. Тип этого свойства, Category, в настоящее время определяется как enum, который выглядит следующим образом:

extension Article {
    enum Category {
        case fullLength
        case quickReads
        case basics
        ...
    }
}

Точно так же, как мы ранее уже перегружали ! с key path специфичным вариантом, мы можем сделать то же самое с оператором ==, и, как и раньше, мы вернем возвращающее Bool замыкание, которое затем может быть напрямую передано в API по типу filter:

func ==<T, V: Equatable>(lhs: KeyPath<T, V>, rhs: V) -> (T) -> Bool {
    return { $0[keyPath: lhs] == rhs }
}

С учетом вышеизложенного теперь мы можем легко фильтровать любую коллекцию, используя сравнение на основе key path, например:

let fullLengthArticles = articles.filter(\.category == .fullLength)

Заключение

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

Как и все, о чем я пишу здесь, на Swift от Sundell, я лично использовал. Я использовал вышеупомянутую технику в нескольких проектах, в которых мне приходилось много фильтровать коллекции, и она проявила себя замечательно, но я бы не стал ее использовать, если бы только у меня не было сильной потребности в такой функциональности.

Чтобы получить более подробный и более продвинутый вариант описанной выше техники, ознакомьтесь с разделом «Предикаты в Swift» и не стесняйтесь присылать мне свои вопросы и комментарии через Twitter или по электронной почте.


Перевод статьи был подготовлен в преддверии старта курса "IOS Developer. Professional".

  • Насколько востребованы iOS-разработчики в период кризиса?

  • Какие требования к соискателям предъявляют компании-работодатели?

  • Какие вопросы задают на собеседовании, и как не допустить ошибку при ответе?

  • Какие знания и навыки необходимы, чтобы выделиться из толпы и обеспечить себе карьерный прогресс?

На все эти вопросы, в рамах бесплатного карьерного вебинара, ответит наш эксперт - Ексей Пантелеев. Также Ексей подробно расскажет о программе курса и процессе обучения. Записаться на вебинар.