[Перевод] Создание пользовательских функций запросов с key paths

03f723e2f13543faa3877ffa9f77d067

Поскольку это довольно строгий статически компилируемый язык, с первого взгляда может показаться, что 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 !(keyPath: KeyPath) -> (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 ==(lhs: KeyPath, 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-разработчики в период кризиса?

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

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

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

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

© Habrahabr.ru