Preventing Invalid File Representations with ~Copyable

I’m designing a library where I want to encourage users to declare a type that represents a file on disk and use it within the same lexical scope in which it’s declared. The reason for this is that my library provides functionality like move, which relocates the file. After such an operation, the original file reference would no longer accurately represent its location on disk.

If users pass a File instance by reference or copy it across different lexical scopes, there’s a risk that it might no longer correctly represent the file it was originally associated with.

Current Design

Here’s a simplified version of my protocol design:

public protocol Directory {
    var location: URL { get }
}

public protocol File {
    /// The name of the file.
    var name: String { get }
    
    /// The enclosing folder containing the file.
    var enclosingFolder: any Directory { get }
}

public extension File {
    /// The location of the file as a URL.
    var location: URL {
        enclosingFolder.location.appending(component: name, directoryHint: .notDirectory)
    }
}

// etc

Issue: Preventing Copies of File

To address this, I wanted to suppress implicit copying by marking File as ~Copyable, allowing me to use consuming for methods that mutate the file. However, I encountered an issue:

Even though I suppress ~Copyable on File, concrete types conforming to File are not required by the compiler to also suppress ~Copyable. This means users could still implement File in a way that allows implicit copying, undermining the goal of ensuring that a file reference is only valid within a certain lexical scope.

Question

• Is this behaviour by design?

• If so, what would be the recommended approach to enforce ~Copyable at the protocol level and ensure that all conforming types follow suit?

• Alternatively, are there better ways to structure this API to prevent accidental misuse while keeping it ergonomic?

Any insights would be greatly appreciated! :rocket:

I would consider instead trying to enforce this through a "box" type. For example, File and Directory could become FileDescriptor and DirectoryDescriptor:

protocol FileDescriptor { /* ... */ }
protocol DirectoryDescriptor { /* ... */ }

struct File<Descriptor: FileDescriptor>: ~Copyable {

    /// The location of the file as a URL.

    var location: URL {
        /* ... */
    }

}

func move(file: consuming File<some FileDescriptor>, to newLocation: some DirectoryDescriptor)  {
    //  ...
}

(Worth noting that it'll be tough to truly prevent them from copying it. They could grab name and enclosingFolder and initialize a new File using them, or any other number of things. But that's somewhat generally true of ~Copyable anyway.)

Is this behaviour by design?

For now, yes.

Currently, types are Copyable by default, so if you wrote

protocol File { }

it's actually:

protocol File: Copyable { }

When you write ~Copyable, you're getting rid of (suppressing) the automatically added Copyable requirement.

File: ~Copyable says "types that implement the File protocol don't have to be copyable", which isn't the same as "types that implement the File protocol must be non-copyable."

protocol File { } /* default ghost Copyable */
protocol File: ~Copyable { } /* no default ghost Copyable */

You can read more about the rationale behind this decision here.

If so, what would be the recommended approach to enforce ~Copyable at the protocol level and ensure that all conforming types follow suit?

As far as I know, right now the only way to do that is through defining your own concrete types that are non-copyable, and defining methods that only accept those concrete types (like we did above.)

I'm still new to non-copyable types so I may be off slightly here, but this is my current understanding.

1 Like

Another option here would be to use a class instead of a noncopyable struct (you can make your protocol descend from AnyObject to require that all conforming types be a class). That way you can be sure that there won’t be unintentional copies of the class. Of course that comes with all the downsides of reference semantics and shared mutable state, but depending on what you want to do it could be the right approach.

1 Like

I settled with this GitHub - nashysolutions/files: A lightweight Swift library for managing file system resources in a protocol-oriented way. Provides abstractions for files and directories, supporting safe and efficient file operations.

1 Like