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

Attribute の最適化 #35

Open
rrbox opened this issue Oct 9, 2023 · 6 comments
Open

Attribute の最適化 #35

rrbox opened this issue Oct 9, 2023 · 6 comments

Comments

@rrbox
Copy link
Owner

rrbox commented Oct 9, 2023

Attribute をビルダーパターンで設計できるようにしていますが、AttributeContext を直接ビルドできるようにして仕舞えばいいでのはないか? という試みです。

不変性も維持したいのであればこんな感じでしょうか。

public struct Attribute {
    let context: AttributeContext
}

public extension Attribute {
    static func fontName(_ value: String) -> Attribute {
        var newContext = AttributeContext()
        newContext.fontName = value
        return .init(context: newContext)
    }
    
    static func fontSize(_ value: NSNumber) -> Attribute {
        var newContext =  AttributeContext()
        newContext.fontSize = value
        return .init(context: newContext)
    }
    
    static func foregroundColor(_ value: Scope.ForegroundColorAttribute.Value) -> Attribute {
        var newContext =  AttributeContext()
        newContext.container.foregroundColor = value
        return .init(context: newContext)
    }
    
    static func backgroundColor(_ value: Scope.BackgroundColorAttribute.Value) -> Attribute {
        var newContext =  AttributeContext()
        newContext.container.backgroundColor = value
        return .init(context: newContext)
    }
    
    static func kern(_ value: Scope.KernAttribute.Value) -> Attribute {
        var newContext =  AttributeContext()
        newContext.container.kern = value
        return .init(context: newContext)
    }
    
    static func tracking(_ value: Scope.TrackingAttribute.Value) -> Attribute {
        var newContext =  AttributeContext()
        newContext.container.tracking = value
        return .init(context: newContext)
    }
    
    static func baselineOffset(_ value: Scope.BaselineOffsetAttribute.Value) -> Attribute {
        var newContext =  AttributeContext()
        newContext.container.baselineOffset = value
        return .init(context: newContext)
    }
}
@rrbox rrbox added the type: Enhancement New feature or request label Oct 9, 2023
@rrbox
Copy link
Owner Author

rrbox commented Oct 9, 2023

上記は生成部分のみでした。モディファイア部分も書いておきます。

public extension Attribute {
   func fontName(_ value: String) -> Attribute {
        var newContext = self.context
        newContext.fontName = value
        return .init(context: newContext)
    }
    
   func fontSize(_ value: NSNumber) -> Attribute {
        var newContext = self.context
        newContext.fontSize = value
        return .init(context: newContext)
    }
    
   func foregroundColor(_ value: Scope.ForegroundColorAttribute.Value) -> Attribute {
        var newContext = self.context
        newContext.container.foregroundColor = value
        return .init(context: newContext)
    }
    
   func backgroundColor(_ value: Scope.BackgroundColorAttribute.Value) -> Attribute {
        var newContext = self.context
        newContext.container.backgroundColor = value
        return .init(context: newContext)
    }
    
   func kern(_ value: Scope.KernAttribute.Value) -> Attribute {
        var newContext = self.context
        newContext.container.kern = value
        return .init(context: newContext)
    }
    
   func tracking(_ value: Scope.TrackingAttribute.Value) -> Attribute {
        var newContext = self.context
        newContext.container.tracking = value
        return .init(context: newContext)
    }
    
   func baselineOffset(_ value: Scope.BaselineOffsetAttribute.Value) -> Attribute {
        var newContext = self.context
        newContext.container.baselineOffset = value
        return .init(context: newContext)
    }
}

@rrbox
Copy link
Owner Author

rrbox commented Oct 9, 2023

#34 を達成する場合は以下のような protocol を使用するといいかもしれません。

public protocol AttributeModifier {
    func modify(context: inout AttributeContext)
}

extension Attribute {
    func modified<Mod: AttributeModifier>(_ mod: Mod) -> Attribute {
        var newContext = self.context
        mod.modify(&newContext)
        .init(context: newContext)
    }
}

例: fontName

struct FontName: AttributeModifier {
    let value: String
    func modify(context: inout AttributeContext) {
        context.fontName = self.value
    }
}

public extension Attribute {
   func fontName(_ value: String) -> Attribute {
        self.modified(FontName(value))
    }
}

@rrbox rrbox changed the title AttributeContext を直接ビルダーできるようにすればいいのでは? Attribute の最適化 Oct 9, 2023
@rrbox
Copy link
Owner Author

rrbox commented Oct 9, 2023

もしこれがうまく機能する場合、enum を使う API と比較して以下のようなメリットがあると思われます。

  • 仕組みがシンプルなので今後の更新が簡単になるかも。
  • 多分メモリ的なパフォーマンスがいい。 <- 最終的にできる Attribute のメモリサイズが固定なので。
  • 多分生成〜使用に必要な実行速度も速い。enum は switch の連続 + こちらは特に加工せず直接利用できる

@rrbox
Copy link
Owner Author

rrbox commented Oct 9, 2023

Attribute を融合(merge)するのが難しいのでは?

@rrbox
Copy link
Owner Author

rrbox commented Oct 10, 2023

この方式では、Attribute を merge することが難しいです。なぜなら、style 設計者がどのプロパティを優先したいかが分からなくなってしまうからです。しかし、方法がないわけではありません。

まず各プロパティに対応する enum を作成します(自動で Hashable になります)。

enum ModifiedProperty {
    case fontName
    case fontSize
    case ...
}

そして、子に列挙体の Set を用意しておきます。

let modifiedPropertyHistory: Set<ModifiedProperty>

あとは Attribute に builder で情報を付加するたびに、プロパティに対応する case をこの Set に挿入していくだけです。

struct Attribute {
    enum ModifiedProperty {
        case fontName
        case fontSize
        case ...
    }
    
    let context: AttributeContext
    let modifiedPropertyHistory: Set<ModifiedProperty>
}

この手法のメリット・デメリットをメモしておきます。

  • メリット
    • 仕組みがシンプルなので、変更に強い。
    • 重複した変更が起こらない。ビルダーに記述することはできるが、データ内部では重複が起きない。
      • ただし、プログラマが重複しないように気をつけて書くことで重複は回避できます。
  • デメリット
    • merge するときに modified property を switch する必要があるのがとても面倒臭い。
      • indirect enum の負荷と大差ないかもです。
    • メモリ節約にはあまりならないかもしれない。
      • ただし、Attribute の情報追加によるデータサイズ増加分は、AttributeType の挿入分のみです
      • Context 部分は固定なので、情報が少なくてもそこそこのデータサイズになります。
    • プログラマが新しい modifier を追加するのが難しいかもしれません。
      • この柔軟性はオプションなので、放棄してもいいかもしれません。
      • 列挙体ではなく、String などにすればいくらでも追加できます。
      • merge の実装が難しそうです。

@rrbox
Copy link
Owner Author

rrbox commented Oct 11, 2023

ひとつ前のコメントのコードを実装するなら、というメモです。

public protocol AttributeModifier {
    var propertyData: Attribute.ModifiedProperty { get }
    func modify(context: inout AttributeContext)
}

extension Attribute {
    func modified<Mod: AttributeModifier>(_ mod: Mod) -> Attribute {
        var newContext = self.context
        var newHistory = self.modifiedPropertyHistory
        mod.modify(&newContext)
        newHistory.insert(mod.propertyData)
        return Attribute(context: newContext, modifiedPropertyHistory: newHistory)
    }
}

Attribute context の merge についてもメモしておきます。

extension AttributeContext {
    // 他のコンテキストを、特定のプロパティについて変異させます
    func modify(_ c: inout AttributeContext, forProperty property: Attribute.ModifiedProperty) {
        switch property {
            case .fontName:
                c.fontName = self.fontName
            case .fontSize:
                c.fontSize = self.fontSize
            case .foregroundColor:
                c.container.foregroundColor = self.foregroundColor
            // ...
            default:
                break
        }
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant