Насколько Swift удобен для DSL?

от автора

Всем привет! 

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

3 моих любимых Proposal:

Когда они только начали обсуждаться, было не понятно, нужны ли они. Но раз за разом после их выхода появлялись рабочие задачи, в которых без них никуда.

Если вам интересно следить за самыми последними новостями iOS разработки и получать подборку интересных статей по этой тематике, тогда вам стоит подписаться на Телеграм-канал iOS Broadcast

Начнем с со старичков@dynamicCallable и @dynamicMemberLookup . Исходя из мотивационной секции proposal, они были добавлены для интеропа с динамическими языками, такими как Python, JavaScript. Но их использование этим не ограничивается.

Dynamic member lookup

@dynamicMemberLookup c нами еще со swift 4.2 и позволяет динамически генерировать свойства у структур или классов, обращаясь к ним на самом деле через subscript.

Например:

struct Channel {     let title: String }  @dynamicMemberLookup struct Author {     let name: String     let channel: Channel      subscript<T>(dynamicMember keyPath: KeyPath<Channel, T>) -> T {         return channel[keyPath: keyPath]     } }  let channel = Channel(title: "iOS Broadcast") let author = Author(name: "Andrey Zonov", channel: channel)  print(author.title) // iOS Broadcast

В данном примере видно, что у структуры автора нет свойства title, но мы смогли к нему обратиться. Где это может быть полезно в жизни? 

Если вы используете Combine, то я надеюсь знаете, что методы sink и assign(to:on:) захватывает сильную ссылку. Чтобы оставить код читаемым и функциональным, я использую простую обертку

titleRequest = generator     .randomTitlePublisher()     .assign(to: \.obj.label.text, on: Unowned(obj: self))  struct Unowned<Object: AnyObject> {     unowned var obj: Object }
⚠️ Важное уточнение ⚠️

Пример с использованием unowned требует зависимости жизненного цикла подписки от жизненного цикла self. Для других случав можно использовать onWeak, который под капотом работает через weak sink

titleRequest = generator     .randomTitlePublisher()     .assign(to: \.label.text, onWeak: self)  extension Publisher where Failure == Never {          func assign<Root: AnyObject>(         to keyPath: ReferenceWritableKeyPath<Root, Output>,         onWeak object: Root     ) -> AnyCancellable {         sink { [weak object] value in             object?[keyPath: keyPath] = value         }     } }

Или более универсальную функцию,

titleRequest = generator     .randomTitlePublisher()     .sink(receiveValue: weak(self, ViewController.assignTitleToLabel))  func weak<T: AnyObject, Argument>(_ obj: T, _ block: @escaping (T) -> (Argument) -> Void) -> (Argument) -> Void {     { [weak obj] a in         obj.map(block)?(a)     } }

Которая позволяет при вызове не захватывать сильной ссылкой self . Но в этом кейсе все портит obj, и от него можно избавиться как раз с помощью @dynamicMemberLookup

titleRequest = generator     .randomTitlePublisher()     .assign(to: \.label.text, on: Unowned(obj: self))  @dynamicMemberLookup struct Unowned<Object: AnyObject> {        unowned var obj: Object          subscript<T>(dynamicMember keyPath: KeyPath<Object, T>) -> T {         obj[keyPath: keyPath]     } }

И это только один из примеров снижения когнитивной нагрузки ваших API для конечных пользователей.

Dynamic callable

@dynamicCallable в Swift позволяет динамически вызывать методы, используя упрощенный синтаксис, добавляя синтаксический сахар в наши API.

Например:

@dynamicCallable final class Conversation {        private var messages = Set<String?>()          @discardableResult     func dynamicallyCall(withKeywordArguments args: KeyValuePairs<String, String>) -> Bool {         for (key, value) in args {             switch key {             case "send":                 return messages.insert(value).inserted             case "delete":                 messages.remove(key)                 return true             case "contains":                 return messages.contains(value)             default:                 return false             }         }         return false     } }  let сonversation = Conversation() сonversation(contains: "Привет!") // false сonversation(send: "Привет!") // true сonversation(contains: "Привет!") // true сonversation(delete: "Привет!") // true сonversation(test: "") // false

В отрыве это кажется не очень нужным, но, к примеру, если вы хотите сделать DSL для сборки URL можно сделать следующее:

@dynamicMemberLookup @dynamicCallable class Dsl<UrlsType> {          private let urls: UrlsType     private var components: [String] = []          init(_ urls: UrlsType) {         self.urls = urls     }          subscript(dynamicMember keyPath: KeyPath<UrlsType, String>) -> Dsl {         components.append(urls[keyPath: keyPath])         return self     }          func dynamicallyCall(withArguments args: [String]) -> Dsl {         components.append(contentsOf: args)         return self     }          func make(replacements: [String: String] = [:]) -> String {         components.joined(separator: "/")     } }

Использовать это можно следующим образом:

struct ProfileUrls {          let user = "user"     let posts = "posts" }  let dsl = Dsl(ProfileUrls()) let url = dsl.user.posts("123").make() // user/posts/123

Это решение может помочь не ошибиться при конструировании URL, если у вас еще не прикручена кодогенерация по контрактам OpenAPI. К тому же его легко покрыть тестами.

Сall as function

И последнее по порядку, но не по важности, это callAsFunction. Опять же, синтаксический сахар, который приравнивает вызов () к вызову callAsFunction. Например:

struct RandomGenerator {     func callAsFunction() -> Int {         .random(in: 0...100)     } }  let generator = RandomGenerator() print(generator())// 58 print(generator())// 46

Универсальный DSL

Эти подходы можно использовать для создания DSL поверх UIKit в стиле SwiftUI:

@dynamicMemberLookup public struct DSL<T> {        let obj: T      public subscript<Value>(dynamicMember keyPath: WritableKeyPath<T, Value>) -> (Value) -> DSL<T> {         { [obj] value in             var object = obj             object[keyPath: keyPath] = value             return DSL(obj: object)         }     }      public subscript<Value>(dynamicMember keyPath: WritableKeyPath<T, Value>) -> (Value) -> T {         { [obj] value in             var object = obj             object[keyPath: keyPath] = value             return object         }     } }  public protocol DSLCompatible {        associatedtype DSLType     var dsl: DSL<DSLType> { get } }  extension DSLCompatible {        public var dsl: DSL<Self> {         DSL(obj: self)     } }  extension NSObject: DSLCompatible {}

После чего можно обращаться ко всем свойствам любого UIKit элемента за счет ротового класса NSObject у них всех:

let label: UILabel = UILabel()     .dsl     .text("Привет")     .font(.preferredFont(forTextStyle: .largeTitle))     .textAlignment(.center)     .textColor(.blue)

Заключение

Начиная с версии 4.2, Swift становится более интероперабельным с динамическими языками, что приносит нам множество возможностей, все они доступны нам уже давно. Что же нас ждет с приходом макросов ?! Главное применять эти фичи языка там, где это действительно требуется, а не искать, где бы их применить.

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


ссылка на оригинал статьи https://habr.com/ru/articles/752614/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *