Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bundle の実装 #50

Closed
rrbox opened this issue Nov 29, 2023 · 7 comments
Closed

Bundle の実装 #50

rrbox opened this issue Nov 29, 2023 · 7 comments
Assignees
Labels
close: Normal 要件が達成された issue tag: v0.2 for version 0.2 type: Enhancement New feature or request
Milestone

Comments

@rrbox
Copy link
Owner

rrbox commented Nov 29, 2023

作れそうだったら作ってみたいです。以下のようなイメージですが、はたして実現可能なのか..?

例: グラフィック用データの定義

struct Transform: Bundle {
    @component var position: Graphic.Position
    @component var zPosition: Graphic.ZPosition
}

struct Sprite: Bundle {
    @bundle var transform: Transform
    @component var size: Graphic.Size
    @component var texture: Graphic.Texture
}

最新の検討案の欄

struct Transform: Bundle {
    var body: some BuilderElement {
        bundleBuilder {
            component(Pos())
            component(ZPos())
        }
    }
}
struct Sprite: Bundle {
    var body: some BuilderElement {
        bundleBuilder {
            component(Size())
            component(Texture())
            bundle(Transform())
        }
    }
}
@rrbox
Copy link
Owner Author

rrbox commented Nov 30, 2023

プロパティラッパーの仕様では、多分無理では?

@rrbox
Copy link
Owner Author

rrbox commented Nov 30, 2023

result builder を使う方法がいいかもしれません。

struct Transform: Bundle {
    var builder: some BundleElements {
        // いい感じにつくれるようにする
        BundleBuilder {
            Position()
            ZPosition()
        }
    }
}

@rrbox
Copy link
Owner Author

rrbox commented Dec 1, 2023

こんな感じの API をまず思いつきました。

struct Transform: Bundle {
    var body: some BuilderElement {
        bundleBuilder {
            component(Pos())
            component(ZPos())
        }
    }
}
struct Sprite: Bundle {
    var body: some BuilderElement {
        bundleBuilder {
            component(Size())
            component(Texture())
            bundle(Transform())
        }
    }
}

Bundlebody プロパティ内部には、Component の二分木が隠蔽されています。この二分木をスキャンすることで、順番に、そして静的ディスパッチによって各コンポーネントにアクセスできます。
欠点として、Builder で配置可能な要素数に制限があることです。要素数を増やしたい場合は、ライブラリ側の実装数を増やす必要があります。


この API の内部についての補足

二分木ノード

まず、二分木のノードの定義です。
Bundle protocol の body プロパティは以下の End, Link のいずれかになる想定です。

protocol BuilderElement {
    
}

struct End<C: Component>: BuilderElement {
    let initialValue: C
}

struct Link<Previous: BuilderElement, Behind: BuilderElement>: BuilderElement {
    let previous: Previous
    let behind: Behind
}

bundle builder

続いて bundleBuilder 関数の実装です。以下の実装に別れています。

  • result builder: 上記のノードを複数個受け取って1つのノードへ圧縮します。
  • bundleBuilder: result builder を隠蔽する関数です。API コードの body で利用されています。

まずは result builder です。
何かしら受け取って、Link へ統合しています。

@resultBuilder
struct BundleBuilder {
    static func buildBlock<T>(_ value: T) -> T {
        value
    }
    
    static func buildBlock<P0, P1>(_ p0: P0, _ p1: P1) -> Link<P0, P1> {
        Link(previous: p0, behind: p1)
    }
    
    static func buildBlock<P0, P1, P2>(_ p0: P0, _ p1: P1, _ p2: P2) -> Link<P0, Link<P1, P2>> {
        Link(previous: p0, behind: Link(previous: p1, behind: p2))
    }
    
    static func buildBlock<P0, P1, P2, P3>(_ p0: P0, _ p1: P1, _ p2: P2, _ p3: P3) -> Link<Link<P0, P1>, Link<P2, P3>> {
        Link(previous: Link(previous: p0, behind: p1), behind: Link(previous: p2, behind: p3))
    }
    
    static func buildBlock<P0, P1, P2, P3, P4>(_ p0: P0, _ p1: P1, _ p2: P2, _ p3: P3, _ p4: P4) -> Link<Link<P0, Link<P1, P2>>, Link<P3, P4>> {
        Link(previous: Link(previous: p0, behind: Link(previous: p1, behind: p2)), behind: Link(previous: p3, behind: p4))
    }
}

次に bundleBuilder 関数の実装です。
ここで要素に BuilderElement の制約をかけます。

func bundleBuilder<T: BuilderElement>(@BundleBuilder _ statement: () -> T) -> T {
    statement()
}

Bundle protocol

Bundle protocol を定義します。
この protocol を実装すると、EndLink といった BuilderElement を定義する関数が完成します。

protocol Bundle {
    associatedtype Body: BuilderElement
    var body: Body { get }
}

このままでは使いづらい(EndLink を直接書きたくない)ので、最後に Component や Bundle を bundleBuilder の要素へ変換する関数を作ります。

func component<T>(_ c: T) -> End<T> {
    End(initialValue: c)
}

func bundle<T: Bundle>(_ b: T) -> T.Body {
    b.body
}

おまけ bundle builder の結果

たとえば、上記の Spritebody はこんなデータになっています。

Link<End<Size>, Link<End<Texture>, Link<End<Pos>, End<ZPos>>>>(
    previous: End<Size>(
        initialValue: Size()
    ),
    behind: Link<End<Texture>, Link<End<Pos>, End<ZPos>>>(
        previous: End<Texture>(
            initialValue: Texture()
        ),
        behind: Link<End<Pos>, End<ZPos>>(
            previous: End<Pos>(
                initialValue: Pos()
            ),
            behind: End<ZPos>(
                initialValue: ZPos()
            )
        )
    )
)

@rrbox rrbox added type: Enhancement New feature or request tag: v0.2 for version 0.2 labels Dec 1, 2023
@rrbox rrbox added this to the version 0.2.0 milestone Feb 16, 2024
@rrbox
Copy link
Owner Author

rrbox commented Apr 19, 2024

Swift macros を使った API が実装できないか検討してください。

@rrbox
Copy link
Owner Author

rrbox commented Apr 19, 2024

Swift macros を使った API が実装できないか検討してください。

イメージですが、型アノテーション付きで全プロパティを定義し、型の記述部分をコード文字列として受け取れるかもしれません。

受け取った型に関するコード文字を使って bundle を組み立てるメソッドを作れるかも。

@rrbox
Copy link
Owner Author

rrbox commented Apr 20, 2024

Macro かけました。

実装

struct BundleMacro: MemberMacro {
    static func expansion(
        of node: AttributeSyntax,
        providingMembersOf declaration: some DeclGroupSyntax,
        in context: some MacroExpansionContext
    ) throws -> [DeclSyntax] {
        let membersAddition = declaration.memberBlock.members
            .compactMap { $0.decl.as(VariableDeclSyntax.self) }
            .compactMap { i in
                i.bindings.first?.pattern.as(IdentifierPatternSyntax.self)?.identifier.text
            }
            .reduce(into: "") { partialResult, identifier in
                partialResult.append("record.addComponent(self.\(identifier))\n")
            }
            .dropLast()
            
        return [
            """
            public func addComponent(forEntity record: EntityRecord) {
                \(raw: membersAddition)
            }
            """
        ]
    }
}

Macro

@attached(member, names: named(addComponent))
public macro Bundle() = #externalMacro(module: "MacroSampleMacros", type: "BundleMacro")

使い方

public protocol Component {
    
}

public class EntityRecord {
    public func addComponent<C: Component>(_ component: C) {
        
    }
}

@Bundle
struct MyBuldle {
    let id: Int
    let name: String
    let position = Position(x: 0, y: 0)
}

func sample() {
    let b = MyBuldle(id: 0, name: "")
    b.addComponent(forEntity: EntityRecord())
}

展開されているコード

@Bundle
struct MyBuldle {
    let id: Int
    let name: String
    let position = Position(x: 0, y: 0)
    
    // 展開コード
    // ```
    public func addComponent(forEntity record: EntityRecord) {
        record.addComponent(self.id)
        record.addComponent(self.name)
        record.addComponent(self.position)
    }
    // ```
}

@rrbox rrbox self-assigned this May 13, 2024
@rrbox rrbox added the status: In progress 現在集中して取り組んでいる issue label Jun 2, 2024
@rrbox rrbox mentioned this issue Jun 2, 2024
rrbox added a commit that referenced this issue Jun 2, 2024
@rrbox rrbox added status: Considering merging マージするか検討中です and removed status: In progress 現在集中して取り組んでいる issue labels Jun 10, 2024
@rrbox
Copy link
Owner Author

rrbox commented Jul 21, 2024

#94
完了

@rrbox rrbox closed this as completed Jul 21, 2024
@rrbox rrbox added close: Normal 要件が達成された issue and removed status: Considering merging マージするか検討中です labels Jul 21, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
close: Normal 要件が達成された issue tag: v0.2 for version 0.2 type: Enhancement New feature or request
Projects
Status: Done
Development

No branches or pull requests

1 participant