티스토리 뷰

반응형

Swift Docs의

- Advanced Operators > Result Builders 

- Attributes > resultBuilder

를 기반으로 하는 내용입니다. 


[1] Result Builder 란?

A result builder is a type you define that adds syntax for creating nested data, like a list or tree, in a natural, 
declarative way. 
A result builder is a type that builds a nested data structure step by step.

 

result builder 는 선언적인 방식 (step by step으로 코드를 작성할 수 있는 방식) 으로 nested data를 만들 수 있게 해줍니다. 

즉 result builder를 사용해서 DSL(Domain Specific Language)을 만들 수 있는데요,

DSL은 복잡한 계층구조를 표현하는 경우에 유용합니다. 

 

class, structure, enumeration에 @resultBuilder 라는 attribute를 붙여서  result builder 로 사용할 수 있습니다. 

(SwiftUI의 @viewBuilder 는 대표적인 result builder의 종류 중 하나 입니다. )

 

 

[2] Result Builder 필요성 

Result Builder의 필요성에 대해 알아보기 위해 아래 예제를 봅시다. 

Single Line을 그리기 위한 몇가지 타입을 정의합니다. 

protocol Drawable {
    func draw() -> String
}

struct Line: Drawable {
    var elements: [Drawable]
    func draw() -> String {
        return elements.map { $0.draw() }.joined(separator: "")
    }
}

struct Text: Drawable {
    var content: String
    init(_ content: String) { self.content = content }
    func draw() -> String { return content }
}

struct Space: Drawable {
    func draw() -> String { return " " }
}

struct Stars: Drawable {
    var length: Int
    func draw() -> String { return String(repeating: "*", count: length) }
}

struct AllCaps: Drawable {
    var content: Drawable
    func draw() -> String { return content.draw().uppercased() }
}

 

이런 식으로 원하는 Single Line을 그릴 수 있습니다. 

let name: String? = "Ravi Patel"
let manualDrawing = Line(elements: [
    Stars(length: 3),
    Text("Hello"),
    Space(),
    AllCaps(content: Text((name ?? "World") + "!")),
    Stars(length: 2),
    ])
print(manualDrawing.draw())
// Prints "***Hello RAVI PATEL!**"

 

 

위 코드는 두가지 아쉬운 점이 있습니다.

 

1. AllCaps 코드가 읽기 어렵습니다.

2. drawing 부분을 build up할 때, switch 나 for loop를 포함해야하는 경우도 있는데, 그럴 수 있는 방법이 없습니다. 

 

이 때, result builder를 사용하면 유용합니다! 

 

[3] Result Builder 만들기 - Step 1 

result builder를 정의하려면  type 선언 위에 @resultBuilder attribute 를 작성해주면 됩니다. 

DrawingBuilder 라는 타입을 정의해주고 @resultBuilder  attribute 를 작성해주겠습니다. 

 

 

result builder는  이 메소드를 반드시 구현해줘야한다고 컴파일 에러가 뜨네요

https://docs.swift.org/swift-book/ReferenceManual/Attributes.html#ID633

 

 

buildBlock(_:) method는 하나의 block에 여러줄의 코드를 작성하는 것을 가능하게 해줍니다. 

이 메소드의 구현으로는 block 안에 있는 여러 component들을 single result로 combine 해주는 코드를 작성해줍니다. 

@resultBuilder
struct DrawingBuilder {
    static func buildBlock(_ components: Drawable...) -> Drawable {
        return Line(elements: components)
    }
}

 

 

그리고 draw 라는 function을 만들고 function 파라미터에 @DrawingBuilder attribute를 적용해줍니다. 

func draw(@DrawingBuilder content: () -> Drawable) -> Drawable {
    return content()
}

그러면 Swift는 function에 전달된 클로져를 result builder가 해당 클로져에서 생성한 값으로 변환하는 작업을 해줍니다. 

정확한 설명을 위해 영어 첨부할게요! 

You can apply the @DrawingBuilder attribute to a function’s parameter, which turns a closure passed to the
function into the value that the result builder creates from that closure
Swift transforms that declarative description of a drawing into a series of calls to the methods on DrawingBuilder to build up the value that’s passed as the function argument.

 

그럼 기존 코드를

let name: String? = "Ravi Patel"
let manualDrawing = Line(elements: [
    Stars(length: 3),
    Text("Hello"),
    Space(),
    AllCaps(content: Text((name ?? "World") + "!")),
    Stars(length: 2),
    ])
print(manualDrawing.draw())
// Prints "***Hello RAVI PATEL!**"

 

더 자연스럽고 선언형인 방식으로 작성할 수 있게 됩니다. (콤마도 작성안해도 됨)

let name: String? = "Ravi Patel"
let manualDrawing = draw {
    Stars(length: 3)
    Text("Hello")
    Space()
    AllCaps(content: Text((name ?? "World") + "!"))
    Stars(length: 2)
}
print(manualDrawing.draw())
// Prints "***Hello RAVI PATEL!**"

 

[4] Result Builder 만들기 - Step 2

아까 AllCaps 부분이 읽기 어렵다고 했으니까 개선해볼게요! 

caps function을 추가하고 

func caps(@DrawingBuilder content: () -> Drawable) -> Drawable {
    return AllCaps(content: content())
}

?? opeartor 대신 if, else 문을 쓰도록 바꿨습니다. 

let name: String? = "Ravi Patel"
let manualDrawing = draw {
    Stars(length: 3)
    Text("Hello")
    Space()
    caps {
        if let name = name {
            Text(name + "!")
        } else {
            Text("World!")
        }
    }
    Stars(length: 2)
}
print(manualDrawing.draw())
// Prints "***Hello RAVI PATEL!**"

 

하지만 if, else 문을 사용하면 아래와 같은 컴파일 에러가 납니다. 

 

 

block 안에서 if else 문을 사용할 수 있게 해주려면 

DrawingBuilder에 두 메소드를 추가해줘야하기 때문입니다. 

 

https://docs.swift.org/swift-book/ReferenceManual/Attributes.html#ID633

 

두 메소드는 단순히 component를 return 해주도록 구현해주면 됩니다. 

@resultBuilder
struct DrawingBuilder {
    static func buildBlock(_ components: Drawable...) -> Drawable {
        return Line(elements: components)
    }
    
    static func buildEither(first component: Drawable) -> Drawable {
        return component
    }
    
    static func buildEither(second component: Drawable) -> Drawable {
        return component
    }
}

 

그럼 컴파일 에러 없이 잘 동작하는 것을 볼 수 있습니다. 

Swift는 caps(_:)에 대한 call을 아래의 코드처럼 변환합니다. 

if-else 블록을  buildEither(first:) 와 buildEither(second:) 에 대한 호출로 변환하는 것을 살펴볼 수 있습니다. 

let capsDrawing = caps {
    let partialDrawing: Drawable
    if let name = name {
        let text = Text(name + "!")
        partialDrawing = DrawingBuilder.buildEither(first: text)
    } else {
        let text = Text("World!")
        partialDrawing = DrawingBuilder.buildEither(second: text)
    }
    return partialDrawing
}

 

[5] Result Builder 만들기 - Step 3

 

더 나아가 block에서 for loop 도 작성할 수 있도록  DrawingBuilder에 buildArray(_:) 도 추가해볼게요!

 

https://docs.swift.org/swift-book/ReferenceManual/Attributes.html#ID633

 

extension DrawingBuilder {
    static func buildArray(_ components: [Drawable]) -> Drawable {
        return Line(elements: components)
    }
}

 

그럼 이제 for loop도 사용할 수 있게 되었습니다 :-) 

let stars = draw {
    Text("Stars:")
    for length in 1...3 {
        Space()
        Stars(length: length)
    }
}
print(stars.draw())
// Prints "Stars: * ** ***"

 

 

[ 더 보면 좋을 것 ]

 

WWDC 2021 > Write a DSL in Swift using result builders 

-  SE-0289

 

✔️ Result-Building Methods 목록이 나와있어요! 

https://docs.swift.org/swift-book/ReferenceManual/Attributes.html#ID633

 

Attributes — The Swift Programming Language (Swift 5.6)

Attributes There are two kinds of attributes in Swift—those that apply to declarations and those that apply to types. An attribute provides additional information about the declaration or type. For example, the discardableResult attribute on a function d

docs.swift.org

 

✔️ SwiftUI 관련 설명도 같이 나와있어요

https://www.hackingwithswift.com/swift/5.4/result-builders

 

Result builders – available from Swift 5.4

Link copied to your pasteboard.

www.hackingwithswift.com

 

✔️  실제 사용 예제를 더 볼 수 있어요

https://github.com/carson-katri/awesome-result-builders

 

GitHub - carson-katri/awesome-result-builders: A list of cool DSLs made with Swift 5.4’s @resultBuilder

A list of cool DSLs made with Swift 5.4’s @resultBuilder - GitHub - carson-katri/awesome-result-builders: A list of cool DSLs made with Swift 5.4’s @resultBuilder

github.com

 

반응형
댓글