Noncopyable Types

The Copyable Protocol

Swift 5.9 introduced the Copyable protocol for types that can be copied. Every type is Copyable by default, so we do not need to explicitly adopt this protocol. Of course types could alredy be copied in earlier versions of Swift, long before Copyable was introduced. In fact, Swift creates copies all the time. After all, this is the first thing we all learned about value types, right?

struct Food {
    var value: String
}

let food = Food(value: "🍏")

// assigning it again implicitly creates a copy of a value type
var copiedFood = food

// updates of the copy don't affect the original:
copiedFood.value = "🍊"

// prints "original: 🍏, copied: 🍊"
print("original: \(food.value), copied: \(copiedFood.value)")

That's exactly the behavior of Copyable. And since everyting conforms by default, code still behaves the same. What's new? We can opt out.

Noncopyable Types

To remove the implicit Copyable conformance from a type, we add ~Copyable to the list of conformances. Usually we pronounce the tilde as "non-", as in "noncopyable". After opting out we get guaranteed unique ownership of the value. It cannot be copied anymore.

struct Credential: ~Copyable {
    var password: String
}

// We cannot re-assign a global noncopyable value.
// A copy cannot be created. And the compiler cannot guarantee that nobody uses the
// original global `credential` property after it was assigned to `theSameCredential`:
let credential = Credential(password: "secret")
let theSameCredential = credential // ❌ Cannot consume noncopyable stored property 'credential' that is global

// we can re-assign local noncopyable values...
func test() {
    let credential = Credential(password: "secret")
    print(credential.password) // βœ… OK here
    
    let credentialCopy = credential // The original `credential` is "consumed" here. We are not allowed to use it afterwards.
    print(credential.password) // ❌ 'credential' used after consume
}

Using Noncopyable Types

When calling a function, its arguments are automatically copied. Obviously that won't work for ~Copyable parameters. We need to tell the compiler what should happen to a noncopyable argument. Such an argument guarantees unique ownership, so we need to decide whether the function should borrow ownership while it is running, or whether it should take ownership away from the caller by consuming the value.

func inspect(_ credential: borrowing Credential) {
    print("The password is:", credential.password)
}

func logIn(_ credential: consuming Credential) {
    print("Logged in with password: \(credential.password)")
}

func test() {
    let credential = Credential(password: "secret")
    
    // `inspect()` borrows `credential`.
    // `test()` maintains ownership when `inspect()` returns, so `credential can be used again.
    inspect(credential)
    
    // `logIn()` takes ownership by consuming `credential`.
    // The `credential` in `test()` cannot be used anymore.
    logIn(credential)
    
    inspect(credential) // ❌ 'credential' used after consume
}

We cannot mutate a borrowed value, it's read-only. Do you remember inout parameters? For Copyable types, the value is copied into a local mutable property. It will be written back to the original place when the function returns. We can use inout parameters wit noncopyable types too! Of course, the value is not copied and written back. Instead, any mutations are applied directly to the original location of the value's data. inout parameters essentially behave like mutatable borrowing parameters.

func update(_ credential: inout Credential, newPassword: String) {
    credential.password = newPassword
}

func test2() {
    var credential = Credential(password: "secret")
    update(&credential, newPassword: "12345")
    
    // we still have ownership of `credentials`, so we can use it:
    print("Updated passsword:", credential.password) // prints "Updated passsword: 12345"
}

Motivation

Copyable types are much simpler to use. We need to add ownership annotations for noncopyable types. And noncopyable types can spread virally if we don't use them carefully, because any value type with noncopyable properties needs to be noncopyable as well. This makes sense β€” how would you create a copy of something if you are not allowed to copy some of its parts?

Copying takes some small amount of processing time. And they consume some memory. This does not matter much in most circumstances because copying is quite efficient. Many larger value types use the Copy-on-Write strategy as an optimization to avoid creating unnecessary copies1.

~Copyable is a tool for performance-critical code. Code where a tight loop is too slow due to excessive copying. Or code that runs in a constrained environment, for example embedded systems.

We can also use ~Copyable to make sure our code is correct and safe. Creating accidental copies may be very dangerous for some values. Can you think of any examples?

  • Credentials: In a login flow we want to make sure the user-entered password does not leak out somehow. We can wrap the string in a non-copyable Credential type. The method for logging in consumes this value. Since swift guarantees unique ownership, we can be confident that this value does not accidentally leak out between creation and consumption. For example, the credential cannot be logged or printed anymore.
  • Transactions in a banking app: Withdrawals and deposits can be modeled as a Transaction to be applied to an account. A customer would be very upset if a withdrawal transaction would be applied twice because the value was accidentally copied somewhere. A noncopyable Transaction prevents this class of bugs at compile time.
  • File handles: A noncopyable file handle ensures unique ownership. This prevents bugs like accidentally closing a file multiple times. We can close the file handle in the type's deinit, which is guaranteed to run only once.

Let's explore a small credentials example:

struct Credential: ~Copyable {
    let user: String
    
    // The password should be private.
    // The only way to read it is via a consuming method.
    private let password: String
    
    init(user: String, password: String) {
        self.user = user
        self.password = password
    }
    
    consuming func getPassword() -> String {
        return password
    }
}

enum Account {
    case loggedOut
    case loggedIn(user: String)
    
    init() {
        self = .loggedOut
    }
    
    mutating func logIn(_ credential: consuming Credential) throws {
        let user = credential.user
        let password = credential.getPassword() // credential is consumed here.
        
        // if password is wrong, throw an error
        // else
        self = .loggedIn(user: user)
    }
}

func test() {
    var account = Account()
    let credential = Credential(user: "mΓΆpelkΓΆtter", password: "secret")
    
    // we cannot accidentally leak the password in the chain between
    // creating the credential and consuming it when logging in.
    // If we try, we get a compiler error:
    print(credential.getPassword()) // ❌ 'credential' consumed more than once
    
    try? account.logIn(credential)
    print(account)
}

Protocols, Generics, Deinit, Discard

We can use noncopyable types as generic arguments since Swift 62. We can also add protocol conformances to noncopyable types. However, protocols implicitly require Copyable conformance by default: protocol P {} is shorthand for protocol P: Copyable {}. We cannot add conformance to P to a noncopyable type unless we remove Copyable from the protocol using ~Copyable:

protocol P: ~Copyable {}

Careful though! This does not mean that all types conforming to P must be noncopyable, or that those types automatically become noncopyable. It just removes the Copyable requirement from the protocol, so that P conformance can be added to both Copyable and noncopyable types:

protocol A {}
protocol B: ~Copyable {}

// `B` does not require `Copyable` conformance, but this type is still `Copyable:
struct C: B {}

// This type cannot be copied, and `B` does not care:
struct NC: B, ~Copyable {}

// Noncopyable types cannot conform to protocols that require `Copyable`:
extension NC: A {} // ❌ Type 'NC' does not conform to protocol 'Copyable'

Let's explore a more concrete example. We start with an Account actor that keeps track of the account's balance:

actor Account {
    private(set) var balance: Int = 0
}

We want to be able to add or remove funds. We model this as applying a Transaction. It's a good idea to to make transactions noncopyable. This prevents bugs caused by accidentally applying a transaction twice.

For some reason3 we decide to write two different transaction types, Deposit and Withdrawal. Both conform to a Transaction protocol:

// Transactions are allowed to be noncopyable
protocol Transaction: ~Copyable {
    
    // an implementation may be, but is not required to be `consuming`.
    consuming func use() -> Int
}

struct Deposit {
    private let amount: UInt
    
    init(amount: UInt) {
        self.amount = amount
    }
}

struct Withdrawal: ~Copyable {
    private let amount: UInt
    
    init(amount: UInt) {
        self.amount = amount
    }
    
    // noncopyable structs may have a deinitializer.
    // We can use it to log a message.
    deinit {
        print("Withdrawal deinitialized before used.")
    }
}

⚠️ Did you notice that Deposit is Copyable? Uh-oh!!! 🚨

Time to add Transaction conformances:

// πŸ’ͺ Yes, we can!
// (conform to `Transaction`, even though `Deposit` is `Copyable`)
extension Deposit: Transaction {
    func use() -> Int {
        return Int(amount)
    }
}

extension Withdrawal: Transaction {
    // The protocol allows us to make this method `consuming`.
    // We don't have to, but we want this behavior to prevent
    // using a transaction twice.
    consuming func use() -> Int {
        let value = -Int(amount)
        
        // We log a message when this transaction is used:
        print("Withdrawal used.")
        
        // ...but we don't want to log the default message in `deinit`.
        // `discard` prevents `deinit` from running:
        discard self
        
        return value
    }
}

Great! Now we can add a generic method that applies some Transaction to an account:

extension Account {
    func apply(_ transaction: consuming some Transaction & ~Copyable) {
        let value = transaction.use()
        balance += value
    }
}

A more traditional way to spell the generic argument is func apply<T: Transaction & ~Copyable>(_ transaction: consuming T) {...}. In both cases the meaning is the same: The argument has to be a Transaction, but it does not need to be Copyable. By default, all generic arguments require Copyable conformance; we use ~Copyable to remove this implicit requirement. In practice, we can now call this method with both copyable and noncopyable transactions.

Let's try it out:

Task {
    let account = Account()
    
    let deposit = Deposit(amount: 100)
    await account.apply(deposit)
    
    let withdrawal = Withdrawal(amount: 50)
    await account.apply(withdrawal) // prints: "Withdrawal used."
    
    // ⚠️ Bug: We apply the same transaction twice!!! πŸ™€
    await account.apply(deposit)
    
    await print("Current balance:", account.balance) // prints: "Current balance: 150" β€” That's too much by 100.
    
    // Withdrawals are noncopyable, so we cannot apply it twice:
    await account.apply(withdrawal) // ❌ 'withdrawal' consumed more than once
    
    let anotherWithdrawal = Withdrawal(amount: 42)
    
    // anotherWithdrawal will be cleaned up at the end of the scope if not
    // consumed otherwise. We would see the message from its `deinit`
    // after "End of demo". Sometimes we want tighter control. We can explicitly
    // `consume` a value. Its memory is freed immediately and the compiler
    // prevents usage afterwards:
    consume anotherWithdrawal // prints: "Withdrawal deinitialized before used."
    
    print("End of demo")
}

Conclusion

~Copyable is an exciting tool in our belt, for fine-grained ownership control, improved efficiency, and to make sure our code is correct. However, we have to be careful: when using ~Copyable with protocols and generics, it does not prescribe noncopyability. Instead, it just opts the protocol or generic out of the requirement to be Copyable.

Have you used noncopyable types, or do you have new ideas how to use them? I'd love to hear from you on Mastodon!


  1. Examples for CoW (Copy-on-Write) types are Collections (`Array`, `Dictionary`, `Set`), `String`, and `Data`. The struct itself is still copied, but the potentially very large amount of data is stored outside of the struct in a reference-counted buffer. For each copy of the struct, the buffer is not copied. Instead, its refernece count is incremened. This way we end up with multiple copies of the struct, each one pointing to the same buffer. This is safe and keeps value semantics as long as nobody writes to the buffer. Writing is only safe, if there is only one reference to the buffer. CoW types check if the buffer is uniquely referenced before writing β€” if not, the buffer is copied. We can create custom CoW types using isKnownUniquelyReferenced(_:). β†©οΈŽοΈŽ

  2. See proposal on Swift Evolution β†©οΈŽοΈŽ

  3. Of course the reason is to demo protocols and generics for noncopyable types! πŸ€“ β†©οΈŽοΈŽ

Tags: