“This is the 15th day of my participation in the First Challenge 2022. For details: First Challenge 2022”

One of the core strengths of the Swift protocols is that they enable us to define shared interfaces that multiple types can follow, which in turn enables us to interact with those types in a very uniform way without having to know what our current underlying type of processing is.

For example, to clearly define an API that allows us to persist a given instance to disk, we might choose to use one of these protocols:

One advantage of defining common apis in this way is that it helps us maintain code consistency, since we can now make any type that should be disk-writable conform to the above protocol, and then we need to implement exactly the same method type for all types.

Another big advantage of the Swift protocols is that they are extensible, which allows us to define various convenience apis for our own protocols as well as externally defined ones — for example, in the standard library or whatever framework we import in the standard library.

In writing these convenience apis, we may also want to mix our currently extended protocols with some functionality provided by other protocols. For example, suppose we want to provide the default implementation of the writeToDisk method of the DiskWritable protocol for types that are also compatible with the Encodable protocol — because the Encodable type can be converted to Data, which we can then automatically writeToDisk.

One way to do this is to have our DiskWritable protocol inherit from Encodable, which in turn will require all conforming types to implement the requirements for both protocols. We can then simply extend DiskWritable to add the default implementation of writeToDisk that we want to provide:

While powerful, the above approach does have a considerable drawback because we’ve now fully coupled the DiskWritable protocol to Encodable — meaning we can no longer use the protocol alone and don’t need any required types to fully implement Encodable, That can be a problem.

A more flexible approach is to keep DiskWritable a completely separate protocol, and instead write a type-constrained extension that adds only our default writeToDisk implementation to our Encodable types as well — like this:

The tradeoff here is that the above approach does require that each type wants to take advantage of our default writeToDisk implementation to explicitly conform to DiskWritable and Encodable, which may not be a big deal, But it might make ita little difficult to discover the default implementation – because it no longer automatically applies to all types that conform to DiskWritable.

However, one solution to the discoverability problem might be to create a convenience type alias (using Swift’s protocol composition operator &) that tells us DiskWritable and Encodable can combine to unlock new features:

When a type conforms to both protocols (using the type alias above, or completely separate), it can now access our default writeToDisk implementation (while still choosing to provide its own custom implementation) :

Combining such protocols can be a very powerful technique, because we’re not limited to adding default implementations of protocol requirements — we can also add entirely new apis to any combination of protocols by simply adding a new method or compute property extension to one of ours.

For example, here we add a second overload of the writeToDisk method, which makes it possible to pass a custom JSONEncoder that will be used when serializing the current instance:

However, we must be careful not to overuse the above pattern, because doing so can cause conflicts if a given type ends up having access to multiple default implementations of the same method.

For illustration, assuming our code base also includes a DataConvertible protocol, we want to extend it with a similar default implementation of writeToDisk — as follows:

Although the two DiskWritable extensions we have created so far both make sense in isolation, if a given type that is compatible with DiskWritable also wants to be compatible with Encodable and DataConvertible (which is likely, Because both protocols are about converting instances to data).

Since the compiler can’t choose which default implementation to use in this case, we have to manually implement our writeToDisk method specifically for each conflict type. Maybe not a big deal, but it can make it hard to tell which method implementation will work for which type, which in turn can make our code feel very unpredictable and difficult to debug and maintain.

So let’s also explore a final alternative to the above problem — implementing our disk-write facilitation API in a dedicated type rather than using protocol extensions. For example, here’s how we can define an EncodingDiskWriter that only requires the types that use it to be Encodable, because writer itself is compatible with DiskWritable:

Therefore, even if the following Document types do not conform to DiskWritable, we can still easily write their data to disk using the new EncodingDiskWriter: