Agent Skills: borrowing & consuming — Parameter Ownership

Use when optimizing large value type performance, working with noncopyable types, or reducing ARC traffic. Covers borrowing, consuming, inout modifiers, consume operator, ~Copyable types.

UncategorizedID: charleswiltgen/axiom/axiom-ownership-conventions

Install this agent skill to your local

pnpm dlx add-skill https://github.com/CharlesWiltgen/Axiom/tree/HEAD/.claude-plugin/plugins/axiom/skills/axiom-ownership-conventions

Skill Files

Browse the full folder contents for axiom-ownership-conventions.

Download Skill

Loading file tree…

.claude-plugin/plugins/axiom/skills/axiom-ownership-conventions/SKILL.md

Skill Metadata

Name
axiom-ownership-conventions
Description
Use when optimizing large value type performance, working with noncopyable types, or reducing ARC traffic. Covers borrowing, consuming, inout modifiers, consume operator, ~Copyable types.

borrowing & consuming — Parameter Ownership

Explicit ownership modifiers for performance optimization and noncopyable type support.

When to Use

Use when:

  • Large value types being passed read-only (avoid copies)
  • Working with noncopyable types (~Copyable)
  • Reducing ARC retain/release traffic
  • Factory methods that consume builder objects
  • Performance-critical code where copies show in profiling

Don't use when:

  • Simple types (Int, Bool, small structs)
  • Compiler optimization is sufficient (most cases)
  • Readability matters more than micro-optimization
  • You're not certain about the performance impact

Quick Reference

| Modifier | Ownership | Copies | Use Case | |----------|-----------|--------|----------| | (default) | Compiler chooses | Implicit | Most cases | | borrowing | Caller keeps | Explicit copy only | Read-only, large types | | consuming | Caller transfers | None needed | Final use, factories | | inout | Caller keeps, mutable | None | Modify in place |

Default Behavior by Context

| Context | Default | Reason | |---------|---------|--------| | Function parameters | borrowing | Most params are read-only | | Initializer parameters | consuming | Usually stored in properties | | Property setters | consuming | Value is stored | | Method self | borrowing | Methods read self |

Patterns

Pattern 1: Read-Only Large Struct

struct LargeBuffer {
    var data: [UInt8]  // Could be megabytes
}

// ❌ Default may copy
func process(_ buffer: LargeBuffer) -> Int {
    buffer.data.count
}

// ✅ Explicit borrow — no copy
func process(_ buffer: borrowing LargeBuffer) -> Int {
    buffer.data.count
}

Pattern 2: Consuming Factory

struct Builder {
    var config: Configuration

    // Consumes self — builder invalid after call
    consuming func build() -> Product {
        Product(config: config)
    }
}

let builder = Builder(config: .default)
let product = builder.build()
// builder is now invalid — compiler error if used

Pattern 3: Explicit Copy in Borrowing

With borrowing, copies must be explicit:

func store(_ value: borrowing LargeValue) {
    // ❌ Error: Cannot implicitly copy borrowing parameter
    self.cached = value

    // ✅ Explicit copy
    self.cached = copy value
}

Pattern 4: Consume Operator

Transfer ownership explicitly:

let data = loadLargeData()
process(consume data)
// data is now invalid — compiler prevents use

Pattern 5: Noncopyable Type

For ~Copyable types, ownership modifiers are required:

struct FileHandle: ~Copyable {
    private let fd: Int32

    init(path: String) throws {
        fd = open(path, O_RDONLY)
        guard fd >= 0 else { throw POSIXError.errno }
    }

    borrowing func read(count: Int) -> Data {
        // Read without consuming handle
        var buffer = [UInt8](repeating: 0, count: count)
        _ = Darwin.read(fd, &buffer, count)
        return Data(buffer)
    }

    consuming func close() {
        Darwin.close(fd)
        // Handle consumed — can't use after close()
    }

    deinit {
        Darwin.close(fd)
    }
}

// Usage
let file = try FileHandle(path: "/tmp/data.txt")
let data = file.read(count: 1024)  // borrowing
file.close()  // consuming — file invalidated

Pattern 6: Reducing ARC Traffic

class ExpensiveObject { /* ... */ }

// ❌ Default: May retain/release
func inspect(_ obj: ExpensiveObject) -> String {
    obj.description
}

// ✅ Borrowing: No ARC traffic
func inspect(_ obj: borrowing ExpensiveObject) -> String {
    obj.description
}

Pattern 7: Consuming Method on Self

struct Transaction {
    var amount: Decimal
    var recipient: String

    // After commit, transaction is consumed
    consuming func commit() async throws {
        try await sendToServer(self)
        // self consumed — can't modify or reuse
    }
}

Common Mistakes

Mistake 1: Over-Optimizing Small Types

// ❌ Unnecessary — Int is trivially copyable
func add(_ a: borrowing Int, _ b: borrowing Int) -> Int {
    a + b
}

// ✅ Let compiler optimize
func add(_ a: Int, _ b: Int) -> Int {
    a + b
}

Mistake 2: Forgetting Explicit Copy

func cache(_ value: borrowing LargeValue) {
    // ❌ Compile error
    self.values.append(value)

    // ✅ Explicit copy required
    self.values.append(copy value)
}

Mistake 3: Consuming When Borrowing Suffices

// ❌ Consumes unnecessarily — caller loses access
func validate(_ data: consuming Data) -> Bool {
    data.count > 0
}

// ✅ Borrow for read-only
func validate(_ data: borrowing Data) -> Bool {
    data.count > 0
}

~Copyable Limitations

Know the constraints before adopting ~Copyable:

| Limitation | Impact | Workaround | |-----------|--------|------------| | Can't store in Array, Dictionary, Set | Collections require Copyable | Use Optional<T> wrapper or manage manually | | Can't use with most generics | <T> implicitly means <T: Copyable> | Use <T: ~Copyable> (requires library support) | | Protocol conformance restricted | Most protocols require Copyable | Use ~Copyable protocol definitions | | Can't capture in closures by default | Closures copy captured values | Use borrowing closure parameters | | No existential support | any ~Copyable doesn't work | Use generics instead |

Common compiler errors when adopting ownership modifiers:

// Error: "Cannot implicitly copy a borrowing parameter"
// Fix: Add explicit `copy` or change to consuming
func store(_ v: borrowing LargeValue) {
    self.cached = copy v  // ✅ Explicit copy
}

// Error: "Noncopyable type cannot be used with generic"
// Fix: Constrain generic to ~Copyable
func use<T: ~Copyable>(_ value: borrowing T) { }  // ✅

// Error: "Cannot consume a borrowing parameter"
// Fix: Change to consuming if you need ownership transfer
func takeOwnership(_ v: consuming FileHandle) { }  // ✅

// Error: "Missing 'consuming' or 'borrowing' modifier"
// Fix: ~Copyable types require explicit ownership on all methods
struct Token: ~Copyable {
    borrowing func peek() -> String { ... }   // ✅ Explicit
    consuming func redeem() { ... }           // ✅ Explicit
}

When NOT to use ~Copyable:

  • If you need collection storage (arrays, dictionaries)
  • If you need to work with existing generic APIs
  • If the type needs broad protocol conformance
  • Prefer consuming func on regular types as a lighter alternative for "use once" semantics

Performance Considerations

When Ownership Modifiers Help

  • Large structs (arrays, dictionaries, custom value types)
  • High-frequency function calls in tight loops
  • Reference types where ARC traffic is measurable
  • Noncopyable types (required, not optional)

When to Skip

  • Default behavior is almost always optimal
  • Small value types (primitives, small structs)
  • Code where profiling shows no benefit
  • API stability concerns (modifiers affect ABI)

Decision Tree

Need explicit ownership?
├─ Working with ~Copyable type?
│  └─ Yes → Required (borrowing/consuming)
├─ Large value type passed frequently?
│  ├─ Read-only? → borrowing
│  └─ Final use? → consuming
├─ ARC traffic visible in profiler?
│  ├─ Read-only? → borrowing
│  └─ Transferring ownership? → consuming
└─ Otherwise → Let compiler choose

Resources

Swift Evolution: SE-0377

WWDC: 2024-10170

Skills: axiom-swift-performance, axiom-swift-concurrency