Това е моят опит с Function Builders в Swift. Как се сблъсках с него, нещата, които направих, за да открия зад кулисите, които правят Function Builder и в крайна сметка достигат до невероятната му забележителност.

И така, без повече шум, нека започнем. Гледах видеото Какво е новото в SwiftWWDC 2019 преди няколко месеца. Харесвах много неща като Implicit Returns, Opaque Result Types & Property Wrappers, но бях поразен от нещо, наречено Function Builders.

Тъй като изглеждаше чуждо, но се чувстваше толкова бързо, нека напиша HTML DSLв чист swift по този начин:

Чувствах се просто страхотно за писането на DSL, защото носи:

  • Синтактична захар: Кратко, но изразително, ясно и лесно за използване, общо взето всичко, което Swift означава ❤️
  • Състав: Инкапсулира йерархични хетерогенни структури от данни
  • „Декларативен стил“
  • Мощността на Swift:Проверка на типа на компилатора и безопасност, автоматично завършване и подчертаване на синтаксиса
  • Край на превключването на контекста

След няколко дни получих шанса да напиша приложение като част от процеса на интервю, изцяло в SwiftUI, който широко използва силата на Function Builder. Тогава разбрах, че е не само страхотно, но е и убийствено 💀.

Сега, когато бях напълно поразен от него, бях развълнуван да разбера зад кулисите, които правят Function Builder. В същото видео на WWDC Anna Zaks от Swift Team добре обяснява как компилаторът трансформира просто изглеждащо затваряне в чист код:

Но аз търсех повече, как компилаторът всъщност прави това. И така, аз се потопих в Swift Evolution Github Page:

и намери предложението Първоначален проект на създателите на функции и съответния PR. Но за съжаление по-голямата част от кода беше на ниво компилатор, което не разбирам. Ако го направите, тогава това определено е златна мина.
Но намерих function_builder.swift, който създава конструктор на функции за Tuple, който бях видял по-рано в документацията, когато работех върху SwiftUI.

И така, сега ми останаха само две опции: SwiftUI Документация и Урок, които използват широко силата на Function Builders. Въпреки че урокът е страхотен, той не говори за това как работи зад кулисите. И документацията беше огромна.

И така, реших да опитам ръцете си на Playground и да последвам документацията, както гласи поговорката на Odiya (родния език на Odisha, индийски щат):

Nije namale, jama darshana nahin
(което в свободен превод означава: „Човек няма да види „Яма“ (господарят на смъртта и справедливостта) до смъртта си“)

Странична бележка:Можете да получите достъп до документация без мрежова връзка, Xcode -› Помощ -› Документация за разработчици:

Разглеждайки документацията на View, се опитах да пресъздам View (основен градивен елемент на SwiftUI Views) Протокол:

Тук идва ViewBuilder в картината, който е конструктор на функции за SwiftUI изгледи.

Документацията на ViewBuilder беше малко прекалено много за мен. Така че, преди да напиша псевдокода, исках да проверя дали е реален и ако е възможно да събера малко допълнителна информация от Playground:

От този прост експеримент събрах малко допълнителна информация:

  • VStack приема едно затваряне, което е доста подобно на всяко друго бързо затваряне.
  • Ако не предоставим никакъв изглед към него, той приема формата на EmptyView.
  • Ако му предоставим един изглед, той приема формата на този Изглед (в нашия случай Текст)
  • Ако му предоставим множество изгледи, той приема формата на TupleView и съхранява изгледите в своята стойност.
  • VStack съхранява нещата в дърво от тип VariadicView.Treeсъс себе си като корен и крайната форма на затваряне, съдържаща изгледите, както е съдържание. Вероятно това е причината, поради която SwiftUI може да приложи модификатор (да речем цвят на фона) от родител към всички негови деца.

Имайки това предвид, опитах да попълня псевдокод за ViewBuilder:

С това място исках да повторя същия експеримент, който направих за SwiftUI VStack, но този път с нашия VStackAlias ​​и нашата реализация на EmptyView & TupleView:

и изчакайте… резултатите бяха доста сходни. Не мога да се съглася повече с констатациите от експеримента на площадката SwiftUI, който направих по-рано:

Въз основа на броя изгледи, предадени на затварянето и техния тип, компилаторът интелигентно синтезира код, за да картографира правилно затварянето към правилния блок за изграждане.

Така че основно всеки от изгледите, предадени на затварянето на VStackAlias, е ефективно израз, синтезиран от компилатора зад капака.
И всичко това е възможно благодарение на специалния персонализиран атрибут @_functionBuilderкойто казва на компилатора да синтезира автоматично блока за затваряне, като разгледа дефиницията на различна компилация блокове в конструктора на функции.

Сега, когато имах добра представа, исках да внедря няколко случая на използване на Function Builder.

Но преди това, ето извадка за вас от предложените понастоящем методи за създател на функции:
В следващите описания Expression означава всеки тип, който е приемлив за израз-изявление (т.е. необработен частичен резултат), Component означава всеки тип, който е приемлив за частичен или комбиниран резултат, а Return означава всеки тип, който е приемливо да бъде върнат в крайна сметка от трансформираната функция.

  • buildExpression(_ expression: Expression) -> Component се използва за повдигане на резултатите от оператори-изрази във вътрешния тип валута Component. Необходимо е само ако DSL иска или (1) да разграничи Expression типове от Component типове или (2) да предостави информация за контекстуален тип за оператори-изрази.
  • buildBlock(_ components: Component...) -> Component се използва за изграждане на комбинирани резултати за повечето блокове с изрази.
  • buildFunction(_ components: Component...) -> Return се използва за изграждане на комбинирани резултати за функционални тела от най-високо ниво. Необходимо е само ако DSL иска да разграничи Component типове от Return типове, напр. ако иска строителите да извършват вътрешно трафик в някакъв тип, който всъщност не иска да изложи на клиентите. Ако не е деклариран, вместо него ще се използва buildBlock.
  • buildDo(_ components: Component...) -> Component се използва за изграждане на комбинирани резултати за do тела на изрази, ако се иска специално третиране за тях. Ако не е деклариран, вместо него ще се използва buildBlock.
  • buildOptional(_ component: Component?) -> Component се използва за изграждане на частичен резултат в обхващащ блок от резултата на незадължително изпълнен подблок. Ако не е деклариран, незадължително изпълнените подблокове са неправилно оформени.
  • buildEither(first: Component) -> Component и buildEither(second: Component) -> Component се използват за изграждане на частични резултати в обхващащ блок от резултата на един от два (или повече, чрез дърво) незадължително изпълнени подблока. Ако и двете не са декларирани, алтернативите вместо това ще бъдат изравнени и обработени с buildOptional; подходът на едно или друго дърво може да е за предпочитане за някои DSL.

Примери:

1. Масив:

Мислех, че ще започна с малко, нещо толкова тривиално като масив.

По принцип исках да проверя дали мога да напиша [7, 8, 6] като нещо подобно:

Това може да изглежда глупаво да се опитва, но повярвайте ми помогна за разбирането ми за по-добрите примери. Добре, нека да видим как постигнах това стъпка по стъпка чрез конструктор на функции.

Започнах с buildBlockза комбинирания резултат от частичните резултати:

След това разширихме initна масив, за да използваме ArrayBuilder, който току-що създадохме:

Това е всичко, сега можем да инициализираме масив с помощта на новия ArrayBuilder и ето как изглежда:

Нека поговорим за intArray(същото важи и за stringArray), компилаторът Swift прави следните стъпки:

// 1 (както е отбелязано в кода)
вижда първата стойност 7 в затварянето, съхранява я в var a

// 2
вижда втората стойност 8 в затварянето, съхранява я в var b

// 3
вижда третата стойност 6 в затварянето, съхранява я в var c

// 4
извиква блок за изгражданес a, b & c по този начин: ArrayBuilder.buildBlock(a, b, c)

След това предаваме синтезираното от компилатора затваряне от (4) към init на Array; при изпълнение на затварянето връща [a, b, c] или [7, 8, 6]; който след това се присвоява на self.

Изобщо не е сложно, нали! 🤡
Вие сте страхотни 😎, нека да видим нещо по-добро сега.

2. NSAttributedString

Първо, нека да видим как пишехме за приписани низове:

Това ще ни даде изхода по-долу в Live View на Playground:

Това е добре, но крайната ми цел беше много по-добра:

Започнах с buildBlockза комбинирания резултат от частичните резултати:

След това добавих buildExpressionза това какви типове изрази поддържа компилаторът. В нашия пример ще приемем низове и ще ги повдигнем до приписани поднизове:

След това е добавена поддръжка за модифициране на атрибути с разширение на NSAttributedString:

Последно, но най-добре, добавих удобство initна NSAttributedString за използване на AttributedStringBuilder, който току-що създадохме:

Това е всичко, сега мога да инициализирам NSAttributedString с помощта на новия AttributedStringBuilder и ето как изглежда:

Компилаторът Swift превежда горния код в следните AttributedStringBuilder извиквания на метод:

  1. buildExpression() с аргумента NSAttributedString.
  2. buildExpression() с аргумента String.
  3. След като всички частични резултати са построени, методът buildBlock() се извиква с всички междинни NSAttributedStrings като аргументи.

Изпълнението на Playground ни даде същия резултат както преди:

3. AlertViewController:

Първо, нека да видим как пишехме за AlertViewController:

Това ще ни даде изхода по-долу в Live View на Playground:

Това е добре, но използването на подход за създаване на функции (както направихме в предишния пример за NSAttributedString) е много по-добро:

4. UIStackView:

Няма да видим как сме го писали, тъй като всички знаем, използваме и обичаме StackView.
И така, нека преминем директно към изпълнението на Function Builder:

Това ни дава изхода по-долу на Live View:

Въпреки че този пример е доста малък, той е достатъчно добър, за да демонстрира как подходът на създателя на функции обхваща контекста на място и истинската йерархична природа на StackView.

4. HTML DSL:
Това е нещо, върху което работя в момента. Това е доста обширен пример, ако имате добра представа за SwiftUI & Function Builder (надявам се, че сега ще имате 😛), вижте кода в Github.

Заключение:

В тази статия обсъдихме подробно Function Builder. Как Apple обедини силата на Swift и DSL.

Мисля, че Swift като език става все по-мощен, елегантен и лесен за използване. Разполагаме с необходимото количество инструменти, за да създадем нещо красиво не само на DSL, но и от страната на сървъра. Сега зависи от нас, разработчиците, да го пренесем на съвсем различно ниво.

Надявам се, че статията ви е харесала и сте научили зад кулисите на Function Builders. Разгледайте цялата детска площадка в Github.

Приятно кодиране и споделяне 😍
Наздраве 🍺
Shahrukh Alam