Hello,
I'd like to pitch an evolution to method availabilty. If a protocol defines a method, then it is not always true that this method should be available on values of any concrete type that adopts the protocol.
protocol P {
// protocol method
func method()
}
struct T: P {
func method() {
// do it
}
}
let p: P = T()
p.method() // yes, of course: method() is defined on P.
let t: T = T()
t.method() // undesired: method() is defined on T, but should be discouraged.
I'll provide below a use case that explains why such restriction can be desired by an API designer.
But before, I must show how the goal is currently impossible to achieve.
Making method
unavailable on T is forbidden by the compiler, with the "Unavailable instance method 'method()' was used to satisfy a requirement of protocol 'P'" error:
struct T: P {
// Compiler error
@available(*, unavailable)
func method() { ... }
}
Making method
deprecated on T "works". It emits a warning, but deprecation is not the exact reason why method should not be used on raw T values:
protocol P {
func method()
}
struct T: P {
@available(*, deprecated)
func method() { }
}
let p: P = T()
p.method() // no warning: OK
let t: T = T()
t.method() // warning: 'method()' is deprecated.
I thus want to pitch two things:
-
Allow a type that conforms to a protocol to forbid the use of a protocol method, with a compiler error. This would be expressed with
@available(*, unavailable)
, or with a new availability name:@available(*, protocolonly)
. -
Allow a type that conforms to a protocol to discourage the use of a protocol method, with a compiler warnibg. This would be expressed with a new availability name:
@available(*, warning)
.
Both could use the renamed
and message
options to provide a more detailed compiler output:
struct T: P {
@available(*, protocolonly, message: "...")
func method() { }
}
struct T: P {
@available(*, warning, renamed: "altMethod")
func method() { }
func altMethod() { }
}
Now is the time for a supporting use case.
It all boils down to concrete types that are narrower than their adopted protocol.
Take a protocol that defines several fine-grained methods in order to accomodate for the full range of needs for a specific domain. Some concrete types may embrace the full subtleties of the protocol. Some other concrete types may be much simpler, and provide, in practice, the same implementation for several protocol requirements.
This happens to the GRDB.swift database library. It has a protocol that defines various methods for accessing an SQLite connection, with distinct scheduling guarantees:
protocol DatabaseWriter {
// Provide ability to read in the database
func read(...)
func unsafeRead(...)
func unsafeReentrantRead(...)
// Provide ability to write in the database
func write(...)
func unsafeWrite(...)
func unsafeReentrantWrite(...)
}
I won't explain here why some methods are deemed "unsafe", and why some methods are marked "reentrant" when others are not. It just happens that GRDB concurrency model needs to expose safe methods for most use cases, and a few other abundantly documented methods for advanced use cases. Thats how the protocol covers the "full range" of SQLite scheduling needs.
Now GRDB ships with two concrete types that adopt the protocol: DatabaseQueue, and DatabasePool. Pools leverage the full protocol subtleties. Queues are much more simple. And the raw use of read
, write
, etc on database queues should be discouraged:
class DatabaseQueue {
// The core method of queues, for both reads and writes:
func inDatabase(...)
}
extension DatabaseQueue: DatabaseWriter {
// Reading methods should be only allowed on iOS 8.2+, OSX 10.10+,
// because it is then possible to prevent writes:
func read(...)
func unsafeRead(...)
func unsafeReentrantRead(...)
// Should be forbidden, renamed "inDatabase"
func write(...)
func unsafeWrite(...)
// OK
func unsafeReentrantWrite(...)
}