The Issue
Consider the following:
/// A simple Event.
public struct Event {}
/// Publishes Events.
public protocol Publisher {
/// Publish an event.
func publish(event: Event)
}
/// My Simple Publisher.
public struct MyPublisher: Publisher {
/// Print the event to be published
public func publish(event: Event) {
print("Event: ", event)
}
}
To use MyPublisher's publish method:
let publisher = MyPublisher()
publisher.publish(event: Event())
// prints: Event: Event()
That implementation is simple and quite functional, but some projects may require that an event be published only from inside the library. So:
/// Publishes Trusted Events.
internal protocol TrustedPublisher: Publisher {
/// The internal method that publishes events.
func _publish(event: Event)
}
extension TrustedPublisher {
/// Do not use this method, it will crush. Use _publish instead.
public func publish(event: Event) {
fatalError("Trusted Publisher can't publish events coming from untrusted sources.")
}
}
/// My Simple Trusted Publisher.
public struct MyTrustedPublisher: TrustedPublisher {
/// Print the event to be published.
internal func _publish(event: Event) {
print("Event: ", event)
}
}
To use MyTrustedPublisher's _publish method:
let trustedPublisher = MyTrustedPublisher()
trustedPublisher._publish(event: Event())
// prints: Event: Event()
But calling the publish method:
trustedPublisher.publish(event: Event())
// fatal error: Trusted Publisher can't publish events coming from untrusted sources.
So since the publish method crashes and the '_publish' method is only available internally, events can't be published outside the module. That means that the user can create their own Publishers while they can still use the TrustedPublisher safely.
The Problem
The disadvantages of such an implementation are what lead to the creation of this proposal. The main problem is that you have to write a lot of boilerplate code. For every new requirement that you want to be inaccessible to the user for your own internal Types, you have to:
- Write the initial Protocol requirement (like the 'publish' method in Publisher).
- Rewrite that requirement in the internal Protocol (like the '_publish' method in TrustedPublisher).
- Cancel the initial requirement with an extension implementation that throws a fatal error (like the 'publish' method in the extension of TrustedPublisher).
Proposal
What I propose is enabling the marking of Protocol requirements with Access Levels. With the proposed change:
/// A simple Event.
public struct Event {}
/// Publishes Events.
public protocol Publisher {
public internal func publish(event: Event)
}
/// My Simple Publisher.
public struct MyPublisher: Publisher {
internal func publish(event: Event) {
print("Event: ", event)
}
}
To use the publish method inside the module (internally) you would write:
let publisher = MyPublisher()
publisher.publish(event: Event())
// prints: Event: Event()
But if you wrote that outside the module where the protocol was declared:
let publisher = MyPublisher()
publisher.publish(event: Event())
// compile error: 'publish' is inaccessible due to 'internal' protection level.
All that's great! But what if the user of the module decides they want to create their own Publisher? Well we'd write:
/// Another Simple Publisher.
public struct AnotherPublisher: Publisher {
// We are required to use the 'public'
// Access Level of the 'publish' requirement
// (we can't use 'internal' here, to see why
// go to the `Access Levels in Depth` section).
public func publish(event: Event) {
print("Event: ", event)
}
}
And to access the Type's publish method:
let publisher = AnotherPublisher()
publisher.publish(event: Event())
// prints: Event: Event()
Detailed Design
So how exactly would Access Levels in Protocol requirements work? Here are some notes about the proposed change:
-
The default requirement Access Level is 'internal', unless the protocol has an Access Level of 'public'. In case the protocol's Access Level is 'public', the requirement's default Access Level becomes public.
-
Requirements cannot have an Access Level of 'private'.
-
Requirements can be accessed only when the accessor's Access Level is sufficient (That includes extensions).
-
Requirement implementations shall have an Access Level equal to or higher than the corresponding requirement's Access Level.
-
When a requirement is marked with more than one Access Level, the lower Access Level available will be chosen.
How does this impact @testable import?
- Requirements with 'internal' Access Level, will be considered 'public' by the Testing Module. All other requirements will remain the same.
That means that a Protocol changes according to the current Access Level. Consider the following Protocol:
/// A Protocol with requirements that have different Access Levels.
public protocol MultipleLevel {
/// Greet internally (only accessible in the current module and the testing module).
internal func internalGreet()
/// Greet publicly (accessible everywhere!).
public func publicGreet()
}
When using this Protocol inside the module it is declared in:
let multipleLevel: MultipleLevel = // ...
multipleLevel.internalGreet()
multipleLevel.publicGreet()
But when used outside the module:
multipleLevel.internalGreet()
// compile error: 'internalGreet' is inaccessible due to 'internal' protection level.
multipleLevel.publicGreet()
In a testing module, the internalGreet
method is exposed. So everything works as if it were used internally:
multipleLevel.internalGreet()
multipleLevel.publicGreet()
Access Levels in Depth
Marking requirements with more than one Access Levels would make this proposal even more powerful.
You might be wondering how this would work. It's quite simple. But it'd be better to explain how marking requirements with Access Levels would work.
-
A requirement can be marked with no Access Levels. In this case the default is used. The default is 'internal', unless the Protocol is 'public', where the default is 'public'.
-
If a requirement is marked with one Access Level, that Access Level is used. It's similar to how structs, enums and classes treat different Access Levels. For example, if a requirement were marked internal, this requirement would only be accessible inside the module the Protocol is declared in.
-
Marking a requirement with more than one Access Level is a bit more complicated. Note that a given Access Level can only be used once. When having multiple Access Levels, the lower Access Level available is used. For Instance, a requirement marked with 'public' and 'internal' would use the 'internal' Access Level when used inside its module (the module the Protocol is declared in). But when used outside that module, it will be required to use 'public'. Also, it's important to point out that a conforming Type can use an Access Level higher than the one chosen. Such an Access Level would be 'public' in the first case. An example of using multiple Access Levels is shown in the
Publisher
Protocol in the Proposal section.
Thanks for reading my proposal!
If you have any feedback please share it with the community, it'd be really appreciated. Your feedback is critical to Swift evolving and improving!