$30 off During Our Annual Pro Sale. View Details »

20分でわかる!速習resultBuilder(iOSDC 2022)

たまねぎ
September 12, 2022

20分でわかる!速習resultBuilder(iOSDC 2022)

たまねぎ

September 12, 2022
Tweet

More Decks by たまねぎ

Other Decks in Technology

Transcript

  1. ɹɹ
    ©︎
    hey, Inc
    20分でわかる!速習resultBuilder


    ヘイ株式会社 テクノロジー部門 モバイルアプリケーション本部


    STORES レジ iOSエンジニア


    たまねぎ(Takuya Yokoyama)
    iOSDC2022

    View Slide

  2. ɹ
    ɹ
    自己紹介
    Profile {


    Name("ͨ·Ͷ͗")


    Twitter("@_chocoyama")


    Work {


    Company("hey inc")


    Product("STORES Regi")


    Role("Mobile Engineer")


    }




    Skill {


    SwiftUI()


    Flutter()


    Compose()


    }


    }

    View Slide

  3. ɹ
    ɹ

    View Slide

  4. ɹ
    ɹ
    発表のゴール
    ●resultBuilderとは何かがわかる


    ●実装方法や使いどころのイメージが持てる

    View Slide

  5. ɹ
    ɹ
    アジェンダ
    ●resultBuilderとは


    ●仕組みと実装方法


    ●導入事例の紹介


    ●まとめ

    View Slide

  6. ɹ
    ɹ
    resultBuilderとは

    View Slide

  7. ɹ
    ɹ
    複数要素の組み合わせ作業を、


    命令的ではなく、宣言的に行えるようにする機能

    View Slide

  8. ɹ
    ɹ
    → 冗長な実装をシンプルに
    複数要素の組み合わせ作業を、


    命令的ではなく、宣言的に行えるようにする機能

    View Slide

  9. ɹ
    ɹ
    resultBuilderとは何か?
    ● Swift5.4で追加されたAttribute


    ● 列挙された要素を収集し、結果の値に変換して取得できる


    ● Swiftで内部DSLを作るための補助機能


    ● 特定領域で効果を発揮するミニチュア言語を作れる

    View Slide

  10. ɹ
    ɹ
    どこで使われてると思いますか?


    (ここから5秒間、心で念じる or コメント or Tweetする時間)

    View Slide

  11. ɹ
    ɹ
    どこで使われている?
    ●ViewBuilder:SwiftUIのレイアウトを記述する時


    ●RegexComponentBuilder:正規表現を記述する時(NEW!)

    View Slide

  12. ɹ
    ɹ
    どこで使われている?
    let word = OneOrMore(.word)


    let emailPattern = Regex {


    Capture {


    ZeroOrMore {


    word


    "."


    }


    word


    }


    "@"


    Capture {


    word


    OneOrMore {


    "."


    word


    }


    }


    }


    struct AlbumDetail: View {


    var album: Album


    var body: some View {


    List(album.songs) { song in


    HStack {


    Image(album.cover)


    VStack(alignment: .leading) {


    Text(song.title)


    Text(song.artist.name)


    .foregroundStyle(.secondary)


    }


    }


    }


    }


    }


    ViewBuilder RegexComponentBuilder

    View Slide

  13. ɹ
    ɹ
    let word = OneOrMore(.word)


    let emailPattern = Regex {


    Capture {


    ZeroOrMore {


    word


    "."


    }


    word


    }


    "@"


    Capture {


    word


    OneOrMore {


    "."


    word


    }


    }


    }


    struct AlbumDetail: View {


    var album: Album


    var body: some View {


    List(album.songs) { song in


    HStack {


    Image(album.cover)


    VStack(alignment: .leading) {


    Text(song.title)


    Text(song.artist.name)


    .foregroundStyle(.secondary)


    }


    }


    }


    }


    }


    どこで使われている?
    ViewBuilder RegexComponentBuilder

    View Slide

  14. ɹ
    ɹ
    let word = OneOrMore(.word)


    View Slide

  15. ɹ
    ɹ
    どのように表現できる?
    1.値の列挙にカンマが必要ない


    2.if文やfor文などの制御構文も利用可能
    @ArrayBuilder


    func someFunction2() ->
    [Int] {


    1


    2




    if true {


    3


    }




    for i in (4...6) {


    i


    }


    }

    View Slide

  16. ɹ
    ɹ
    何ができる?
    1.値の列挙にカンマが必要ない


    2.if文やfor文などの制御構文も利用可能
    @ArrayBuilder


    func someFunction2() ->
    [Int] {


    1


    2




    if true {


    3


    }




    for i in (4...6) {


    i


    }


    }
    ಉ݁͡Ռ͕ಘΒΕΔ
    = [1, 2, 3, 4, 5, 6]
    func someFunction1() -> [Int] {


    var result = [


    1,


    2


    ]




    if true {


    result.append(3)


    }




    for i in (4...6) {


    result.append(i)


    }




    return result


    }

    View Slide

  17. ɹ
    ɹ
    なぜ必要?
    struct AlbumDetail: View {


    var album: Album


    var body: some View {


    List(album.songs) { song in


    HStack {


    Image(album.cover)


    VStack(alignment: .leading) {


    Text(song.title)


    Text(song.artist.name)


    .foregroundStyle(.secondary)


    }


    }


    }


    }


    }


    View Slide

  18. ɹ
    ɹ
    resultBuilderを使わずに同じことを実現する場合
    struct AlbumDetail: View {


    var album: Album


    var body: some View {


    List(album.songs) { song in


    HStack {


    Image(album.cover)


    VStack(alignment: .leading) {


    Text(song.title)


    Text(song.artist.name)


    .foregroundStyle(.secondary)


    }


    }


    }


    }


    }


    struct AlbumDetail: View {


    var album: Album


    var body: some View {


    var items: [View] = []


    for song in album.songs {


    let hStack = HStack(


    contents: [


    Image(album.cover),


    VStack(


    alignment: .leading,


    contents [


    Text(song.title),


    Text(song.artist.name)


    .foregroundStyle(.secondary)


    ]


    )


    ]


    )


    items.append(hStack)


    }


    return List(contents: items)


    }


    }


    ※ ͜ͷίʔυ͸͋͘·ͰΠϝʔδͰ͢

    View Slide

  19. ɹ
    ɹ
    もしresultBuilderを使わずに同じことを実現すると
    ● 制御文や変数宣言でコードが肥大化


    ● 階層構造が複雑で、結果が想像しづらい


    ● 変更コストが大きい
    struct AlbumDetail: View {


    var album: Album


    var body: some View {


    var items: [View] = []


    for song in album.songs {


    let hStack = HStack(


    contents: [


    Image(album.cover),


    VStack(


    alignment: .leading,


    contents [


    Text(song.title),


    Text(song.artist.name)


    .foregroundStyle(.secondary)


    ]


    )


    ]


    )


    items.append(hStack)


    }


    return List(contents: items)


    }


    }


    ※ ͜ͷίʔυ͸͋͘·ͰΠϝʔδͰ͢

    View Slide

  20. ɹ
    ɹ
    resultBuilder
    何ができる?     シンプルな記述で、複数要素の収集と結果値への変換ができる


    どこで使われている? SwiftUIやRegexなどで用いられている


    なぜ必要?      可読性・保守性の向上が期待できるため

    View Slide

  21. ɹ
    ɹ
    SwiftUI, Regex以外に使い道あるの?


    View Slide

  22. ɹ
    ɹ
    SwiftUI, Regex以外に使い道あるの?





    カスタムのresultBuilderの作成で


    実装効率を高められるケースがある

    View Slide

  23. ɹ
    ɹ
    例えばこういう時
    let mas = NSMutableAttributedString(string: “")


    mas.append(NSAttributedString(


    string: "Hello world",


    attributes: [


    .font: UIFont.systemFont(ofSize: 24),


    .foregroundColor: UIColor.red


    ]


    ))


    mas.append(NSAttributedString(


    string: "\n"


    ))


    mas.append(NSAttributedString(


    string: "with Swift",


    attributes: [


    .font: UIFont.systemFont(ofSize: 20),


    .foregroundColor: UIColor.orange


    ]


    ))


    View Slide

  24. ɹ
    ɹ
    • 要素指定と関係ない記述が多い


    • 構造がわかりづらい


    • 変更による修正量が多い
    例えばこういう時
    let mas = NSMutableAttributedString(string: “")


    mas.append(NSAttributedString(


    string: "Hello world",


    attributes: [


    .font: UIFont.systemFont(ofSize: 24),


    .foregroundColor: UIColor.red


    ]


    ))


    mas.append(NSAttributedString(


    string: "\n"


    ))


    mas.append(NSAttributedString(


    string: "with Swift",


    attributes: [


    .font: UIFont.systemFont(ofSize: 20),


    .foregroundColor: UIColor.orange


    ]


    ))


    View Slide

  25. ɹ
    ɹ
    例えばこういう時
    let mas = NSMutableAttributedString(string: “")


    mas.append(NSAttributedString(


    string: "Hello world",


    attributes: [


    .font: UIFont.systemFont(ofSize: 24),


    .foregroundColor: UIColor.red


    ]


    ))


    mas.append(NSAttributedString(


    string: "\n"


    ))


    mas.append(NSAttributedString(


    string: "with Swift",


    attributes: [


    .font: UIFont.systemFont(ofSize: 20),


    .foregroundColor: UIColor.orange


    ]


    ))


    let attributedString = NSAttributedString {


    AText("Hello world")


    .font(.systemFont(ofSize: 24))


    .foregroundColor(.red)


    LineBreak()


    AText("with Swift")


    .font(.systemFont(ofSize: 20))


    .foregroundColor(.orange)


    }
    • 要素指定と関係ない記述が多い


    • 構造がわかりづらい


    • 変更による修正量が多い
    • 要素指定以外の記述が少ない


    • 構造がわかりやすい


    • 変更による修正量が少ない
    // https://github.com/ethanhuang13/NSAttributedStringBuilder

    View Slide

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


    Data {


    [UInt8(0)]


    UInt8(1)


    Int8(2)


    "\u{03}"


    Int16(1284)


    if dataClause {


    CustomData()


    }


    }
    // Parsing


    let capture = HTML {


    TryCapture("#hello") { (element: HTMLElement?) -> String? in


    return element?.textContent


    }




    Local("#group") {


    Capture("h1", transform: \.textContent)


    Capture("h2", transform: \.textContent)


    }


    }


    // Networking


    Request {


    Url("https://jsonplaceholder.typicode.com/posts")


    Method(.post)


    Header.ContentType(.json)


    Body(Json([


    "title": "foo",


    "body": "bar",


    "usedId": 1


    ]).stringified)


    }
    // GraphQL


    Operation(.query) {


    Add(\.country,
    alias: "canada") {


    Add(\.name)


    Add(\.continent) {


    Add(\.name)


    }


    }.code("CA")


    }


    View Slide

  27. ɹ
    ɹ
    仕組みと実装方法

    View Slide

  28. ɹ
    ɹ
    作る
    @ArrayBuilder


    func someFunction() -> [Int] {


    1


    2


    3


    }


    someFunction() // [1, 2, 3]

    View Slide

  29. ɹ
    ɹ
    作る
    @ArrayBuilder


    func someFunction() -> [Int] {


    1


    2


    3


    }


    someFunction() // [1, 2, 3]
    @resultBuilder struct ArrayBuilder {


    static func buildBlock(_ components: Int...) -> [Int] {


    components


    }


    }

    View Slide

  30. ɹ
    ɹ
    @resultBuilder struct ArrayBuilder {


    static func buildBlock(_ components: Int...) -> [Int] {


    components


    }


    }
    作る
    resultBuilderアノテーションの付与

    View Slide

  31. ɹ
    ɹ
    @resultBuilder struct ArrayBuilder {


    static func buildBlock(_ components: Int...) -> [Int] {


    components


    }


    }
    作る
    buildBlock関数を定義

    View Slide

  32. ɹ
    ɹ
    @resultBuilder struct ArrayBuilder {


    static func buildBlock(_ components: Int...) -> [Int] {


    components


    }


    }
    作る
    引数として受け取れる型 & 返却値の型を指定

    View Slide

  33. ɹ
    ɹ
    @resultBuilder struct ArrayBuilder {


    static func buildBlock(_ components: Int...) -> [Int] {


    components


    }


    }
    作る
    インプット値をアウトプット値に変換

    View Slide

  34. ɹ
    ɹ
    @ArrayBuilder


    func someFunction() -> [Int] {


    1


    2


    3


    }


    someFunction() // [1, 2, 3]
    @resultBuilder struct ArrayBuilder {


    static func buildBlock(_ components: Int...) -> [Int] {


    components


    }


    }
    使う
    作成したクラス名をアノテーションでfunc, var, etcに付与する

    View Slide

  35. ɹ
    ɹ
    @ArrayBuilder


    func someFunction() -> [Int] {


    1


    2


    3


    }


    someFunction() // [1, 2, 3]
    @resultBuilder struct ArrayBuilder {


    static func buildBlock(_ components: Int...) -> [Int] {


    components


    }


    }
    使う
    buildBlockの引数の型に対応した値を列挙

    View Slide

  36. ɹ
    ɹ
    @ArrayBuilder


    func someFunction() -> [Int] {


    1


    2


    3


    }


    someFunction() // [1, 2, 3]
    @resultBuilder struct ArrayBuilder {


    static func buildBlock(_ components: Int...) -> [Int] {


    components


    }


    }
    使う
    指定された返却値の型の値が返される

    View Slide

  37. ɹ
    ɹ
    @ArrayBuilder


    func someFunction() -> [Int] {


    1


    2


    3


    }


    someFunction() // [2, 4, 6]
    @resultBuilder struct ArrayBuilder {


    static func buildBlock(_ components: Int...) -> [Int] {


    components.map { $0 * 2 }


    }


    }
    使う
    任意の処理を挟むことができる

    View Slide

  38. ɹ
    ɹ
    最低限必要な実装はこれだけ、ただし…

    View Slide

  39. ɹ
    ɹ
    制御構文への対応
    @ArrayBuilder


    func function1() -> [Int] {


    // Closure containing control flow statement cannot be used with result builder 'ArrayBuilder'


    if true {


    1


    }


    }


    @ArrayBuilder


    func function2() -> [Int] {


    // Closure containing control flow statement cannot be used with result builder 'ArrayBuilder'


    for i in (2..<10) {


    i


    }


    }
    If,forͳͲͷར༻ʹ͸ɺରԠ͢ΔbuildXXXؔ਺Λ࣮૷͢Δඞཁ͕͋Δ

    View Slide

  40. ɹ
    ɹ
    buildXXX関数
    ؔ਺ ݺͼग़͞ΕΔՕॴ
    buildBlock ϒϩοΫ͝ͱ
    buildExpression ࣜ͝ͱ
    buildOptional elseͷͳ͍෼ذ
    buildEither
    elseͷ͋Δ෼ذ
    switchʹΑΔ෼ذ
    buildArray ܁Γฦ͠
    buildFinalResult ݁Ռͷ஋ͷ׬੒
    buildLimitedAvailability #availableʹΑΔ෼ذ
    buildPartialResult ϒϩοΫͷߦ͝ͱ
    ※ ΦʔόʔϩʔυՄೳ

    View Slide

  41. ɹ
    ɹ
    buildBlock
    @ArrayBuilder


    func someFunction() -> [Int] {


    1


    2


    3


    }

    View Slide

  42. ɹ
    ɹ
    buildBlock
    func someFunction() -> [Int] {


    let v0 = 1


    let v1 = 2


    let v2 = 3


    return ArrayBuilder.buildBlock(v0, v1, v2)


    }

    View Slide

  43. ɹ
    ɹ
    buildBlock
    func someFunction() -> [Int] {


    let v0 = 1


    let v1 = 2


    let v2 = 3


    return ArrayBuilder.buildBlock(v0, v1, v2)


    }
    @resultBuilder struct ArrayBuilder {


    static func buildBlock(_ components: Int...) -> [Int] {


    components


    }


    }

    View Slide

  44. ɹ
    ɹ
    分岐への対応
    @ArrayBuilder


    func someFunction() -> [Int] {


    1


    if Bool.random() {


    2


    3


    }


    }

    View Slide

  45. ɹ
    ɹ
    分岐への対応
    func someFunction() -> [Int] {


    let v0: Int = 1


    let v1


    if Bool.random() {


    2


    3


    }


    return ArrayBuilder.buildBlock(v0, v1)


    }

    View Slide

  46. ɹ
    ɹ
    分岐への対応
    func someFunction() -> [Int] {


    let v0: Int = 1


    let v1


    if Bool.random() {


    let v1_0 = 2


    let v1_1 = 3


    let v1_block: [Int] = ArrayBuilder.buildBlock(v1_0, v1_1)


    }


    return ArrayBuilder.buildBlock(v0, v1)


    }

    View Slide

  47. ɹ
    ɹ
    分岐への対応
    func someFunction() -> [Int] {


    let v0: Int = 1


    let v1


    if Bool.random() {


    let v1_0 = 2


    let v1_1 = 3


    let v1_block: [Int] = ArrayBuilder.buildBlock(v1_0, v1_1)


    }


    return ArrayBuilder.buildBlock(v0, v1)


    }
    ؔ਺ ݺͼग़͞ΕΔՕॴ
    buildOptional elseͷͳ͍෼ذ

    View Slide

  48. ɹ
    ɹ
    分岐への対応
    func someFunction() -> [Int] {


    let v0: Int = 1


    let v1


    if Bool.random() {


    let v1_0 = 2


    let v1_1 = 3


    let v1_block: [Int] = ArrayBuilder.buildBlock(v1_0, v1_1)


    v1 = ArrayBuilder.buildOptional(v1_block)


    } else {


    v1 = ArrayBuilder.buildOptional(nil)


    }


    return ArrayBuilder.buildBlock(v0, v1)


    }

    View Slide

  49. ɹ
    ɹ
    分岐への対応
    func someFunction() -> [Int] {


    let v0: Int = 1


    let v1: [Int]


    if Bool.random() {


    let v1_0 = 2


    let v1_1 = 3


    let v1_block: [Int] = ArrayBuilder.buildBlock(v1_0, v1_1)


    v1 = ArrayBuilder.buildOptional(v1_block)


    } else {


    v1 = ArrayBuilder.buildOptional(nil)


    }


    return ArrayBuilder.buildBlock(v0, v1)


    }
    @resultBuilder struct ArrayBuilder {


    static func buildBlock(_ components: Int...) -> [Int] {


    components


    }


    static func buildOptional(_ component: [Int]?) -> [Int] {


    component ?? []


    }


    }

    View Slide

  50. ɹ
    ɹ
    分岐への対応
    func someFunction() -> [Int] {


    let v0: Int = 1


    let v1: [Int]


    if Bool.random() {


    let v1_0 = 2


    let v1_1 = 3


    let v1_block: [Int] = ArrayBuilder.buildBlock(v1_0, v1_1)


    v1 = ArrayBuilder.buildOptional(v1_block)


    } else {


    v1 = ArrayBuilder.buildOptional(nil)


    }


    return ArrayBuilder.buildBlock(v0, v1)


    }
    @resultBuilder struct ArrayBuilder {


    static func buildBlock(_ components: Int...) -> [Int] {


    components


    }


    static func buildOptional(_ component: [Int]?) -> [Int] {


    component ?? []


    }


    }
    Error!

    View Slide

  51. ɹ
    ɹ
    分岐への対応
    func someFunction() -> [Int] {


    let v0: Int = 1


    let v1: [Int]


    if Bool.random() {


    let v1_0 = 2


    let v1_1 = 3


    let v1_block: [Int] = ArrayBuilder.buildBlock(v1_0, v1_1)


    v1 = ArrayBuilder.buildOptional(v1_block)


    } else {


    v1 = ArrayBuilder.buildOptional(nil)


    }


    return ArrayBuilder.buildBlock(v0, v1)


    }
    @resultBuilder struct ArrayBuilder {


    static func buildBlock(_ components: Int...) -> [Int] {


    components


    }


    static func buildOptional(_ component: [Int]?) -> [Int] {


    component ?? []


    }


    }
    Int [Int]

    View Slide

  52. ɹ
    ɹ
    分岐への対応
    func someFunction() -> [Int] {


    let v0: Int = 1


    let v1: [Int]


    if Bool.random() {


    let v1_0 = 2


    let v1_1 = 3


    let v1_block: [Int] = ArrayBuilder.buildBlock(v1_0, v1_1)


    v1 = ArrayBuilder.buildOptional(v1_block)


    } else {


    v1 = ArrayBuilder.buildOptional(nil)


    }


    return ArrayBuilder.buildBlock(v0, v1)


    }
    @resultBuilder struct ArrayBuilder {


    static func buildBlock(_ components: Int...) -> [Int] {


    components


    }


    static func buildOptional(_ component: [Int]?) -> [Int] {


    component ?? []


    }


    }
    ᶃ v0Λ[Int]ʹม׵ͯ͠ܕ͕ἧ͏Α͏ʹ͢Δ
    ᶄ buildBlockͰ[Int]ͷՄม௕Ҿ਺Λ౉ͤΔΑ͏ʹ͢Δ

    View Slide

  53. ɹ
    ɹ
    ① v0を[Int]に変換して型が揃うようにする
    func someFunction() -> [Int] {


    let v0: Int = 1


    let v1: [Int]


    if Bool.random() {


    let v1_0 = 2


    let v1_1 = 3


    let v1_block: [Int] = ArrayBuilder.buildBlock(v1_0, v1_1)


    v1 = ArrayBuilder.buildOptional(v1_block)


    } else {


    v1 = ArrayBuilder.buildOptional(nil)


    }


    return ArrayBuilder.buildBlock(v0, v1)


    }
    @resultBuilder struct ArrayBuilder {


    static func buildBlock(_ components: Int...) -> [Int] {


    components


    }


    static func buildOptional(_ component: [Int]?) -> [Int] {


    component ?? []


    }


    }
    ᶃ v0Λ[Int]ʹม׵ͯ͠ܕ͕ἧ͏Α͏ʹ͢Δ

    View Slide

  54. ɹ
    ɹ
    ① v0を[Int]に変換して型が揃うようにする
    func someFunction() -> [Int] {


    let v0: [Int] = ArrayBuilder.buildExpression(expression: 1)


    let v1: [Int]


    if Bool.random() {


    let v1_0: [Int] = ArrayBuilder.buildExpression(expression: 2)


    let v1_1: [Int] = ArrayBuilder.buildExpression(expression: 3)


    let v1_block: [Int] = ArrayBuilder.buildBlock(v1_0, v1_1)


    v1 = ArrayBuilder.buildOptional(v1_block)


    } else {


    v1 = ArrayBuilder.buildOptional(nil)


    }


    return ArrayBuilder.buildBlock(v0, v1)


    }
    @resultBuilder struct ArrayBuilder {


    static func buildExpression(_ expression: Int) -> [Int] {


    [expression]


    }




    static func buildBlock(_ components: Int...) -> [Int] {


    components


    }




    static func buildOptional(_ component: [Int]?) -> [Int] {




    ؔ਺ ݺͼग़͞ΕΔՕॴ
    buildExpression ࣜ͝ͱ
    ᶃ v0Λ[Int]ʹม׵ͯ͠ܕ͕ἧ͏Α͏ʹ͢Δ

    View Slide

  55. ɹ
    ɹ
    ② buildBlockで[Int]の可変長引数を渡せるようにする
    func someFunction() -> [Int] {


    let v0: [Int] = ArrayBuilder.buildExpression(expression: 1)


    let v1: [Int]


    if Bool.random() {


    let v1_0: [Int] = ArrayBuilder.buildExpression(expression: 2)


    let v1_1: [Int] = ArrayBuilder.buildExpression(expression: 3)


    let v1_block: [Int] = ArrayBuilder.buildBlock(v1_0, v1_1)


    v1 = ArrayBuilder.buildOptional(v1_block)


    } else {


    v1 = ArrayBuilder.buildOptional(nil)


    }


    return ArrayBuilder.buildBlock(v0, v1)


    }
    @resultBuilder struct ArrayBuilder {


    static func buildExpression(_ expression: Int) -> [Int] {


    [expression]


    }




    static func buildBlock(_ components: Int...) -> [Int] {


    components


    }




    static func buildOptional(_ component: [Int]?) -> [Int] {




    ᶄ buildBlockͰ[Int]ͷՄม௕Ҿ਺Λ౉ͤΔΑ͏ʹ͢Δ

    View Slide

  56. ɹ
    ɹ
    ② buildBlockで[Int]の可変長引数を渡せるようにする
    func someFunction() -> [Int] {


    let v0: [Int] = ArrayBuilder.buildExpression(expression: 1)


    let v1: [Int]


    if Bool.random() {


    let v1_0: [Int] = ArrayBuilder.buildExpression(expression: 2)


    let v1_1: [Int] = ArrayBuilder.buildExpression(expression: 3)


    let v1_block: [Int] = ArrayBuilder.buildBlock(v1_0, v1_1)


    v1 = ArrayBuilder.buildOptional(v1_block)


    } else {


    v1 = ArrayBuilder.buildOptional(nil)


    }


    return ArrayBuilder.buildBlock(v0, v1)


    }
    @resultBuilder struct ArrayBuilder {


    static func buildExpression(_ expression: Int) -> [Int] {


    [expression]


    }




    static func buildBlock(_ components: [Int]...) -> [Int] {


    components.flatMap { $0 }


    }




    static func buildOptional(_ component: [Int]?) -> [Int] {




    ᶄ buildBlockͰ[Int]ͷՄม௕Ҿ਺Λ౉ͤΔΑ͏ʹ͢Δ

    View Slide

  57. ɹ
    ɹ
    最終的な実装
    func someFunction() -> [Int] {


    let v0: [Int] = ArrayBuilder.buildExpression(expression: 1)


    let v1: [Int]


    if Bool.random() {


    let v1_0: [Int] = ArrayBuilder.buildExpression(expression: 2)


    let v1_1: [Int] = ArrayBuilder.buildExpression(expression: 3)


    let v1_block: [Int] = ArrayBuilder.buildBlock(v1_0, v1_1)


    v1 = ArrayBuilder.buildOptional(v1_block)


    } else {


    v1 = ArrayBuilder.buildOptional(nil)


    }


    return ArrayBuilder.buildBlock(v0, v1)


    }
    @resultBuilder struct ArrayBuilder {


    static func buildExpression(_ expression: Int) -> [Int] {


    [expression]


    }




    static func buildBlock(_ components: [Int]...) -> [Int] {


    components.flatMap { $0 }


    }




    static func buildOptional(_ component: [Int]?) -> [Int] {


    component ?? []


    }


    }

    View Slide

  58. ɹ
    ɹ
    最終的な実装
    @ArrayBuilder


    func someFunction() -> [Int] {


    1


    if Bool.random() {


    2


    3


    }


    }
    @resultBuilder struct ArrayBuilder {


    static func buildExpression(_ expression: Int) -> [Int] {


    [expression]


    }




    static func buildBlock(_ components: [Int]...) -> [Int] {


    components.flatMap { $0 }


    }




    static func buildOptional(_ component: [Int]?) -> [Int] {


    component ?? []


    }


    }

    View Slide

  59. ɹ
    ɹ
    その他制御構文への対応
    for i in (1...3) {


    i


    }
    var v0: [Int] = []


    for i in (1...3) {


    // ...


    v0.append(i_block)


    }


    let v0_array = ArrayBuilder.buildArray(v0)
    if Bool.random() {


    1


    } else {


    2


    }
    if Bool.random() {


    // ...


    v0 = ArrayBuilder.buildEither(first: v1_block)


    } else {


    // ...


    v0 = ArrayBuilder.buildEither(second: v2_block)


    }
    WWDC2021: Write a DSL in Swift using result builders

    View Slide

  60. ɹ
    ɹ
    導入事例の紹介

    View Slide

  61. ɹ
    ɹ
    STORES レジ
    ● 2021.06リリースしたiPadアプリ


    ● 店舗運営を支援するPOSシステムを提供


    ● フルSwiftUI
    SwiftUI+GraphQLͰϓϩμΫτͷܧଓతͳഁյʹཱͪ޲͔͏ʢ iOSDC2021ʣ

    View Slide

  62. ɹ
    ɹ
    レシート印刷

    View Slide

  63. ɹ
    ɹ
    レシート印刷の流れ
    ϨΠΞ΢τ༻ಠࣗXML ը૾σʔλ
    ϓϦϯλʔ΁ૹ৴
    ม׵

    View Slide

  64. ɹ
    ɹ
    ϨΠΞ΢τ༻ಠࣗXML ը૾σʔλ
    ϓϦϯλʔ΁ૹ৴
    ม׵
    独自ルールに基づいたXML文字列の生成






    ͝ར༻໌ࡉ








    ϔομʔϝοηʔδ





    ΞΠςϜ໊1


    2000ԁ


    2000ԁ








    ※ 社内の別アプリでの仕組みを、STORES レジ用に移植

    View Slide

  65. ɹ
    ɹ
    独自ルールに基づいたXML文字列の生成






    ͝ར༻໌ࡉ








    ϔομʔϝοηʔδ





    ΞΠςϜ໊1


    2000ԁ


    2000ԁ








    var xml = ""


    if let src = receiptSetting.logoImageURL?.absoluteString {


    xml += "”


    }


    xml += """


    ͝ར༻໌ࡉ





    """


    for item in items {


    if item.quantity >= 2 {


    let label = "(item.price) x \(item.quantity)"


    xml += """


    \(item.name)


    \(item.totalPrice)


    “""


    } else {


    xml += ""


    xml += "\(item.totalPrice)"


    xml += ""


    }


    }


    // ~ লུ ~


    xml += ""


    return xml


    View Slide

  66. ɹ
    ɹ
    XMLベタ書きの問題
    1.型安全でなく、想定外の文字があると壊れる


    2.冗長な記述が多く、レイアウト構造と非対応で見通しが悪い


    3.変更があった場合、改修コストが高い


    4.指定可能な値の把握がしづらい
    var xml = ""


    if let src = receiptSetting.logoImageURL?.absoluteString {


    xml += "”


    }


    xml += """


    ͝ར༻໌ࡉ





    """

    View Slide

  67. ɹ
    ɹ
    var xml = ""


    if let src = receiptSetting.logoImageURL?.absoluteString {


    xml += "”


    }


    xml += """


    ͝ར༻໌ࡉ





    """
    XMLベタ書きの問題
    ཁ͸ɺݸʑͷXMLཁૉΛऩूͯ͠ɺจࣈྻΛੜ੒͍ͯ͠Δ͚ͩ
    → resultBuilderͰͷཁૉͷऩूʹద͍ͯ͠Δ

    View Slide

  68. ɹ
    ɹ
    実装の概要
    protocol ReceiptElement {


    var body: String { get }


    }
    各要素を同一視するためのProtocolを定義

    View Slide

  69. ɹ
    ɹ
    実装の概要
    protocol ReceiptElement {


    var body: String { get }


    }
    struct Text: ReceiptElement {


    var body: String {


    “some text"


    }


    }
    struct Image: ReceiptElement {


    var body: String {


    “”


    }


    }
    各要素を同一視するためのProtocolを定義
    利用できるXML要素を準拠させる
    ※ આ໌ͷͨΊ؆ུԽͨ͠ίʔυΛࡌ͍ͤͯ·͢

    View Slide

  70. ɹ
    ɹ
    実装の概要
    @resultBuilder struct ReceiptBuilder {


    static func buildBlock(_ components: ReceiptElement...) -> [ReceiptElement] {


    components


    }




    static func buildBlock(_ components: [ReceiptElement]...) -> [ReceiptElement] {


    components.flatMap { $0 }


    }




    static func buildExpression(_ expression: ReceiptElement) -> [ReceiptElement] {


    [expression]


    }




    static func buildExpression(_ expression: [ReceiptElement]) -> [ReceiptElement] {


    expression


    }




    static func buildEither(first component: [ReceiptElement]) -> [ReceiptElement] {


    component


    }




    static func buildEither(second component: [ReceiptElement]) -> [ReceiptElement] {


    component


    }




    static func buildOptional(_ component: [ReceiptElement]?) -> [ReceiptElement] {


    component ?? []


    }




    static func buildArray(_ components: [[ReceiptElement]]) -> [ReceiptElement] {


    components.flatMap { $0 }


    }


    }
    ReceiptElementをresultBuilderで収集

    View Slide

  71. ɹ
    ɹ
    実装の概要
    struct Receipt {


    let buildElements: () -> [ReceiptElement]




    init(@ReceiptBuilder buildElements: @escaping () -> [ReceiptElement]) {


    self.buildElements = buildElements


    }




    var xml: String {


    let elements = buildElements()


    return Tag(.receipt).build(VStack(elements).body)


    }




    func image(width: CGFloat) async throws -> UIImage {


    let builder = ReceiptImageBuilder(width: width)


    return try await builder.build(xml: xml)


    }


    }


    View Slide

  72. ɹ
    ɹ
    実装の概要
    struct Receipt {


    let buildElements: () -> [ReceiptElement]




    init(@ReceiptBuilder buildElements: @escaping () -> [ReceiptElement]) {


    self.buildElements = buildElements


    }




    var xml: String {


    let elements = buildElements()


    return Tag(.receipt).build(VStack(elements).body)


    }




    func image(width: CGFloat) async throws -> UIImage {


    let builder = ReceiptImageBuilder(width: width)


    return try await builder.build(xml: xml)


    }


    }



    1.resultBuilderをイニシャライザで受け取る

    View Slide

  73. ɹ
    ɹ
    実装の概要
    struct Receipt {


    let buildElements: () -> [ReceiptElement]




    init(@ReceiptBuilder buildElements: @escaping () -> [ReceiptElement]) {


    self.buildElements = buildElements


    }




    var xml: String {


    let elements = buildElements()


    return Tag(.receipt).build(VStack(elements).body)


    }




    func image(width: CGFloat) async throws -> UIImage {


    let builder = ReceiptImageBuilder(width: width)


    return try await builder.build(xml: xml)


    }


    }




    1.resultBuilderをイニシャライザで受け取る


    2.受け取ったresultBuilderでReceiptElementの配列を収集

    View Slide

  74. ɹ
    ɹ
    実装の概要
    struct Receipt {


    let buildElements: () -> [ReceiptElement]




    init(@ReceiptBuilder buildElements: @escaping () -> [ReceiptElement]) {


    self.buildElements = buildElements


    }




    var xml: String {


    let elements = buildElements()


    return Tag(.receipt).build(VStack(elements).body)


    }




    func image(width: CGFloat) async throws -> UIImage {


    let builder = ReceiptImageBuilder(width: width)


    return try await builder.build(xml: xml)


    }


    }





    1.resultBuilderをイニシャライザで受け取る


    2.受け取ったresultBuilderでReceiptElementの配列を収集


    3.収集した要素をXML文字列に変換

    View Slide

  75. ɹ
    ɹ
    実装の概要
    1.resultBuilderをイニシャライザで受け取る


    2.受け取ったresultBuilderでReceiptElementの配列を収集


    3.収集した要素をXML文字列に変換


    4.XML文字列を画像に変換
    struct Receipt {


    let buildElements: () -> [ReceiptElement]




    init(@ReceiptBuilder buildElements: @escaping () -> [ReceiptElement]) {


    self.buildElements = buildElements


    }




    var xml: String {


    let elements = buildElements()


    return Tag(.receipt).build(VStack(elements).body)


    }




    func image(width: CGFloat) async throws -> UIImage {


    let builder = ReceiptImageBuilder(width: width)


    return try await builder.build(xml: xml)


    }


    }






    View Slide

  76. ɹ
    ɹ
    Before: XMLベタ書き
    var xml = ""


    if let src = receiptSetting.logoImageURL?.absoluteString {


    xml += "”


    }


    xml += """


    ͝ར༻໌ࡉ





    """


    for item in items {


    if item.quantity >= 2 {


    let label = "(item.price) x \(item.quantity)"


    xml += """


    \(item.name)


    \(item.totalPrice)


    “""


    } else {


    xml += ""


    xml += "\(item.totalPrice)"


    xml += ""


    }


    }


    // ~ লུ ~


    xml += ""


    return xml


    View Slide

  77. ɹ
    ɹ
    Before: XMLベタ書き
    Receipt {


    if let src = receiptSetting.logoImageURL {


    Image(src: src)


    .maxHeight(96)


    }




    Title(“͝ར༻໌ࡉ")


    Divider()




    VStack(spacing: 2) {


    for item in items {


    if item.quantity >= 2 {


    Text(item.name)


    .truncated()




    LabelText(label: "\(item.price) x \(item.quantity)",


    content: item.totalPrice)


    } else {


    LabelText(label: item.name,


    content: item.totalPrice)


    .truncated()


    }


    }


    }


    }


    View Slide

  78. ɹ
    ɹ
    Before: XMLベタ書き
    let receiptImage = try await Receipt {


    if let src = receiptSetting.logoImageURL {


    Image(src: src)


    .maxHeight(96)


    }




    Title(“͝ར༻໌ࡉ")


    Divider()




    VStack(spacing: 2) {


    for item in items {


    if item.quantity >= 2 {


    Text(item.name)


    .truncated()




    LabelText(label: "\(item.price) x \(item.quantity)",


    content: item.totalPrice)


    } else {


    LabelText(label: item.name,


    content: item.totalPrice)


    .truncated()


    }


    }


    }


    }.image(width: 240)


    View Slide

  79. ɹ
    ɹ
    let receiptImage = try await Receipt {


    if let src = receiptSetting.logoImageURL {


    Image(src: src)


    .maxHeight(96)


    }




    Title(“͝ར༻໌ࡉ")


    Divider()




    VStack(spacing: 2) {


    for item in items {


    if item.quantity >= 2 {


    Text(item.name)


    .truncated()




    LabelText(label: "\(item.price) x \(item.quantity)",


    content: item.totalPrice)


    } else {


    LabelText(label: item.name,


    content: item.totalPrice)


    .truncated()


    }


    }


    }


    }.image(width: 240)


    var xml = ""


    if let src = receiptSetting.logoImageURL?.absoluteString {


    xml += "”


    }


    xml += """


    ͝ར༻໌ࡉ





    """


    for item in items {


    if item.quantity >= 2 {


    let label = "(item.price) x \(item.quantity)"


    xml += """


    \(item.name)


    \(item.totalPrice)


    “""


    } else {


    xml += ""


    xml += "\(item.totalPrice)"


    xml += ""


    }


    }


    // ~ লུ ~


    xml += ""


    return xml


    After: resultBuilder利用

    View Slide

  80. ɹ
    ɹ
    resultBuilderの活用による効果
    1. 型安全でなく、想定外の文字があると壊れる


    2. シンプルな表現で、レイアウト構造と対応して見通しが良い


    3. 変更する際に、コンパイラの助けを借りられる


    4. 指定可能な値をコード補完で把握できる
    XMLベタ書き resultBuilderを活用
    苦しい実装 楽しい実装

    View Slide

  81. ɹ
    ɹ
    まとめ

    View Slide

  82. ɹ
    ɹ
    ● ボイラープレートを削減し、冗長なコードを圧倒的にシンプル化できる


    ● コードの見通しが良くなり、結果が予想しやすくなる


    ● 宣言的な記述が主になるため、変更が容易になる
    resultBuilderの利点

    View Slide

  83. ɹ
    ɹ
    ● DSLは利用者に学習を求めることになる


    ● 無闇な導入は過度な抽象化をもたらし、逆にコストが高くなる恐れもある


    ● 機能の利用自体が目的とならないよう、使い所の見極めは必要
    注意点

    View Slide

  84. ɹ
    ɹ
    ● 特定の結果を生成するために、複数要素を組み合わせる場合


    ● ユースケースに応じて、様々な組み合わせが生まれる場合


    ● 要素の追加や削除などが行われやすい場合
    resultBuilderの効果的な使い所

    View Slide

  85. ɹ
    ɹ
    ありがとうございました!
    ࠾༻΋΍ͬͯΔΑʂ

    View Slide