Closed rrbox closed 4 months ago
プロパティラッパーの仕様では、多分無理では?
result builder を使う方法がいいかもしれません。
struct Transform: Bundle {
var builder: some BundleElements {
// いい感じにつくれるようにする
BundleBuilder {
Position()
ZPosition()
}
}
}
こんな感じの 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())
}
}
}
Bundle
の body
プロパティ内部には、Component の二分木が隠蔽されています。この二分木をスキャンすることで、順番に、そして静的ディスパッチによって各コンポーネントにアクセスできます。
欠点として、Builder で配置可能な要素数に制限があることです。要素数を増やしたい場合は、ライブラリ側の実装数を増やす必要があります。
まず、二分木のノードの定義です。
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
}
続いて bundleBuilder
関数の実装です。以下の実装に別れています。
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 を定義します。
この protocol を実装すると、End
や Link
といった BuilderElement
を定義する関数が完成します。
protocol Bundle {
associatedtype Body: BuilderElement
var body: Body { get }
}
このままでは使いづらい(End
や Link
を直接書きたくない)ので、最後に Component や Bundle を bundleBuilder
の要素へ変換する関数を作ります。
func component<T>(_ c: T) -> End<T> {
End(initialValue: c)
}
func bundle<T: Bundle>(_ b: T) -> T.Body {
b.body
}
たとえば、上記の Sprite
の body
はこんなデータになっています。
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()
)
)
)
)
Swift macros を使った API が実装できないか検討してください。
Swift macros を使った API が実装できないか検討してください。
イメージですが、型アノテーション付きで全プロパティを定義し、型の記述部分をコード文字列として受け取れるかもしれません。
受け取った型に関するコード文字を使って bundle を組み立てるメソッドを作れるかも。
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)
}
// ```
}
完了
作れそうだったら作ってみたいです。以下のようなイメージですが、はたして実現可能なのか..?
例: グラフィック用データの定義
最新の検討案の欄