First Step to Swift DSL
In the last article I introduced how to create DSL in Kotlin. Swift also has a function for DSL , which is called Function Builder.
In this article, I'll show closer a look at Function Builder, and create HTML-like dsl pattern with it.
Function Builder
Function Builder
is one of the main components in SwiftUI. This factor was added in Swift5.1, and some frameworks use this for making DSL (HTML, XML..etc...)
This means it is worth learning to know how to create Swift-DSL for understanding the backgronud of SwiftUI.
The following code is a very simple sample using Function Builder
.
@_functionBuilder struct Builder { static func buildBlock(_ s1: String, _ s2: String) -> Int { s1.count + s2.count } } //@Builder annotation is important. func make(@Builder block: () -> Int) -> Float { Float(block()).squareRoot() } let value = make { "AB" "CDEF" } print(value) //2.4494898 (=√6)
This code makes no sense but made to understand FunctionBuilder
behavior. You will notice:
- In
make
function calling, it passes 2 Strings. But not separeted by comma. buildBlock
function takes 2 String arguments, and returns 1 Int.make
function takes 1 argument, () -> Int. and returns Float.- The result is a floating number.
make
function itself doesn't accept 2 Strings, but it works. This behavior shows how FunctionBuilder
works. Swift-compiler converted this code to following:
make(block: {(_) -> Int in Builder.buildBlock("AB", "CDEF") }) //block() will return 6, the length of "ABCDEF"
If you change the arguments of buildBlock
to One-String, the code can't be built. You need to change the make
argument, too.
This is the basic of FunctionBuilder. Let's go next step, Creating HTML-like DSL
Creating Tree structure
To create Html-dsl, we need to create a Struct that describes tree-structure.
struct Node { let name: String let children: [Node] }
To simplify the explanation, this article doesn't care about texts inside tags (it means creating tree only in this post).
Second, let's think about the final code of this program. It's similar to SwiftUI, like this:
Node("html") { Node("head") { Node("meta") { } } Node("body") { Node("h1") { } } }
This indicates the requirements of Node's initializer.
- First argument is tag-name.
- Second argument is children-lambda using FunctionBuilder.
struct Node { let name: String let children: [Node] init(_ name: String, @Builder children: () -> [Node]) { self.name = name self.children = children() } }
Last, create Builder
itself. The work of it is very simple. Creating Node-Array from arguments.
@_functionBuilder struct Builder { //for empty lambda static func buildBlock() -> [Node] { [Node]() } static func buildBlock(_ n1: Node) -> [Node] { [n1] } static func buildBlock(_ n1: Node, _ n2: Node) -> [Node] { [n1, n2] } static func buildBlock(_ n1: Node, _ n3: Node) -> [Node] { [n1, n2, n3] } }
Completed!
Adding more builder-functions
FunctionBuilder
has more funcitons. If you have experienced SwiftUI, you may make if in view-tree.
HStack { if flag { Text("ABC") } else { Text("DEF") } }
Of course FunctionBuilder
supports this patterns. Using buildIf. This function takes a nullable argument.
@_functionBuilder struct Builder { static func buildIf(_ n1: Node?) -> Node { } }
Unfortunatelly this will not work. We need to arrange Node
and [Node]
as same type by Protocol adapting.
protocol INode { } struct Node: Node { } extension Arary: Node where Element == INode { } @_functionBuilder struct Builder { static func buildBlock() -> INode { [INode]() } static func buildBlock(_ n1: INode) -> INode { n1 } static func buildBlock(_ n1: INode, _ n2: INode) -> INode { [n1, n2] } static func buildIf(_ content: INode?) -> INode { content ?? [INode]() } } make { Node("A") { } if flag { Node("B") } }
It works! But this pattern supports if
only, if-else
requires buildEither
.
@_functionBuilder struct Builder { static func buildEither(first: INode?) -> INode { content ?? [INode]() } static func buildEither(second: INode?) -> INode { content ?? [INode]() } }
buildEither(first:)
will be called if conditions are met, otherwise buildEither(second:)
will be called.
Conclusion
With these feature, Swift can support DSL syntax to create complex structure like XML or HTML. And learining FunctionBuilder
helps understand SwiftUI background.
If you want to know more about FunctionBuilder, please check SwiftUI ViewBuilder or awesome-function-builders.
Thanks for reading!