Swift Protocols Wishlist
If I were Supreme Swift Potentate, there are a few things I’d change about how Swift deals with protocols, and how this gets manifest in the standard library.
In no particular order, here they are:
-
If you’re adopting a protocol (especially one from the standard library), the tools don’t provide any help whatsoever in knowing what you actually have to implement. Because of this, it’s pretty much flat-out impossible¹ to make your own value-typed data structure (like, say, an AVL tree or whatever) that conforms to all of the expected data structure-y protocols.
-
I wish protocols were generic instead of having associated types. For example, I wish I could declare:
func processTheseIntegers(_ integers: Collection<Int>) { ... }
Instead of having to make the whole thing generic:
func processTheseIntegers<C: Collection>(_ integers: C) where C.Element == Int { ... }
-
We don’t have generic protocols, because instead we have these things called Protocols with Associated Types (“PATs”). PATs are things that look great at first blush. Just take a look at how succinct this is!
protocol Requestable { associatedtype ResponseType: InitializableFromData ... }
It’s so easy declare that “I have this request, and it has a related response type, and this response value has to conform to this protocol. Awesome!
Except…
The rules around arrays and stuff in Swift mean you can’t create an array of anything that happens to be
Requestable
:var currentRequests: Array<Requestable> // not allowed
In order to do something like this, you have to create type erasers or propagate the associated type of the protocol out as a generic. It’s maddening and it sucks the life out of you. Basically any time you find yourself using a PAT, you know that you’re either going to have to create a type eraser, or you’re going to have to make things generic that have no business being generic. They’re these parasitic things that end up infesting your code in ways you really don’t want.
-
I wish the compiler generated type erasers for me. In the case of the
Requestable
stuff above, the common way to work around the problem of heterogenous arrays is to make them homogenous using type erasers. A type eraser is a value that hides some of this stuff and “homogenizes” the types to the type system. For examples of this, see pretty much anything in the standard library whose name starts withAny
, likeAnyHashable
. Writing type erasers manually is an exercise in pain. You have to create a couple of layers of indirection and do some weird things that I can never remember off the top of my head and always have to google. It’s grunt work, and the compiler should do it for me. -
I wish there were a way to say “this protocol can only be adopted by value types”. We have the opposite, to say “this protocol can only be adopted by reference types”, which is great for defining
weak var delegate: MyDelegateProtocol?
.But there are times when you want a protocol-typed value, but don’t want to allow mutability. Normally in Swift you define immutability with
let
andvar
. However, you can’t do that with protocols. With a protocol, everything is avar
, because it can be adopted by a reference type, and reference types have no restrictions on internal mutability. This is inherently unsafe, if you allow for things to change out from underneath you. This is whyArray
andString
and everything became value types inSwift
, because their internal-mutability creates for some nasty bugs in Objective-C, where they’re reference types.² -
This one is pretty niche, but occasionally you deal with a lot of single-requirement protocols, like this:
protocol YearHaving { var year: Int { get } } protocol MonthHaving { var month: Int { get } } protocol DayHaving { var day: Int { get } }
It’d be nice if there were a shorthand way to define a struct that just has all of those fields. Maybe something like this:
auto struct YearMonthDay: YearHaving, MonthHaving, DayHaving { }
That way you wouldn’t have to re-declare all of the properties you’re supposed to have, and they’d all come in as
let
properties (because they only have{ get }
), and then you get the default compiler-provided initializer of.init(year: Int, month: Int, day: Int)
. Yeah this is pretty specific, but when you need it, you need it. -
These last ones are all related, and they’re all really big. I wish I could provide default implementations of protocol methods that assign to self. For example:
protocol URLRequestable { var url: URL { get } }
There are a bunch of different kinds of URLs. It’d be nice if I could do:
extension URLRequestable { init(url: URL) { switch url.scheme { case "mailto": self = EmailRequest(url: url) case "ftp": self = FTPRequest(url: url) case "http": self = HTTPRequest(url: url) default: self = URLRequest(url: url) } } }
This is related to a concept in Objective-C called “class clusters”. A class cluster is basically an interface for a type, with a bunch of varying implementations that depend on certain conditions. To me, defining a class cluster’s interface via a protocol feels far more “swifty” than using an abstract superclass (like Objective-C), but there’s also no way to make it truly swifty and use an initializer to create your cluster instance; you have to use a static factory method if you want to do that.
-
I wish I could extend a protocol to conform to another protocol. For example:
protocol URLRequestable { var url: URL { get } }
If I know that all
URLRequestable
things have a protocol, then I should be able to do:extension URLRequestable: Hashable { static func ==(lhs: URLRequestable, rhs: URLRequestable) -> Bool { return lhs.url == rhs.url } var hashValue: Int { return url.hashValue } }
That would be super useful, because I could get incoming
URLRequestable
values, and then toss them in aSet
to help ensure I don’t execute duplicate requests. Sadly, you cannot do this. I don’t know why. (Usually when I ask, I get told something intensely inscrutable that includes the word “existential” in it) -
Adding these all up leads me to my last wish: I wish that the API defined by the standard library was actually all just protocols. I wish
String
andArray
andDictionary
andSet
were protocols. If you had protocol clusters, you could still construct things like you do now, but you’d just get back private implementations ofprotocol String
or whatever. This would mean that, when I ask aDictionary<String, Foo>
for its.keys
, I could just get back aSet<String>
and not aDictionary<String, Foo>.Keys
. Similarly, when I ask for a.lazy.something
, I don’t have to get back some weirdLazyRandomAccessCollection
or whatever; I could just get anArray<Something>
that happens to have a lazy implementation.
I look at these things on this list and think “wow, Swift would be awesome if it had all of this”. I really really wish it did; especially the last one. And maybe someday it’ll have a couple of these, but I get depressed when I think that it’s probably highly unlikely it’ll ever have all of them.
¹ - yeah, I know it’s not impossible. But doing it “right” requires way too much work to even figure out what “right” means.
² - yeah, I know you can make a struct that is internally mutable by having it wrap a reference type.