プログラマ英語学習日記

プログラミングと英語学習のまとめなど

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!