We will discuss Swift Protocol in depth from practical techniques and implementation principles.

In the previous part of this article, we focused on practical techniques. We discussed Type Erasure, Opaque Types, Generics, and Phantom Types in detail through examples of a Protocol related compilation error. They are helpful for writing better and more elegant Swift code.

This post is also published on my personal blog

lead


Swift advocates Protocol Oriented Programming (POP), so Protocol is particularly important in Swift.

But this article is not about the use of Protocol or POP.

Our discussion starts with a compilation error:

Protocol ‘Equatable’ can only be used as a generic constraint because it has Self or associated type requirements.

This compilation error should be familiar to Swift developers.

The literal meaning is easy to understand: protocols that contain Self or associated types can only be used as generic constraints, not as types alone.

According to?

Because Swift is a type-safe language.

According to?

The above explanation is “correct nonsense” and misses the point.

GitHub – zxfcumtcs/MarkdownDemo: Swift Protocol Demo

As shown above, MarkdownEditor is an editor in Markdown format.

To handle the different Markdown formats, we define the protocol MarkdownBuilder, which is exposed to the business as a public interface:

public protocol MarkdownBuilder: Equatable, Identifiable {
  var style: String { get }
  func build(from text: String) -> String
}
Copy the code

Due to the need for parsing, MarkdownBuilder inherits the Equatable protocol.

If we use MarkdownBuilder directly as a type, such as var Builder: MarkdownBuilder, this error will be reported.

Because Equatable has Self requirements: The LHS and RHS parameters of the == operator must be of the same type (the exact type, not just the Equatable).

public protocol Equatable {
  static func == (lhs: Self, rhs: Self) -> Bool
}
Copy the code

If a Protocol with Self requirements/Associated Type is allowed as a Type, the following situation occurs, and the compiler is unable to do anything about it:

let lhs: Equatable = 1 // Int let rsh: Equatable = "1" // String lhs == rsh // ? ! , different types of values can be evaluatedCopy the code

The same is true for Associated types:

// Because the phone number can be expressed in multiple formats // abstracted protocol, // protocol PhoneNumberVerifier {associatedType Phone func verify(_ model: Phone) -> Bool } struct IntPhoneNumberVerifier: PhoneNumberVerifier { func verify(_ model: Int) -> Bool { // do some verify } } struct StrPhoneNumberVerifier: PhoneNumberVerifier { func verify(_ model: String) -> Bool { // do some verify } } let verifiers: [PhoneNumberVerifier] = [...]  verifiers.forEach { verifier in verifier.verify(???) // How to pass the parameters here? Int? String? Compiler cannot guarantee type safety}Copy the code

The bottom line is that Protocol is a runtime feature, and the Self Requirements/Associated Type that comes with it needs to be guaranteed at compile time. The result must be cool

Generics is a compile-time feature that specifies the specific Type of the generic at compile time, so protocols with Self Requirements /Associated types can only be used as constraints.

Type Erasure


Going back to the Markdown editor mentioned in the previous section: MarkdownEditor, we implemented MarkdownBuilder in four formats:

Public var id: String {style} // struct ItalicsBuilder: MarkdownBuilder { public var style: String { "*Italics*" } public func build(from text: String) -> String {"*(text)*"}} struct Struct Builder: MarkdownBuilder {public var style: String { "**Bold**" } public func build(from text: String) -> String {"**(text)**"}} // struct StrikethroughBuilder: MarkdownBuilder { public var style: String { "~Strikethrough~" } public func build(from text: String) -> String {"~(text)~"}} // struct LinkBuilder: MarkdownBuilder {public var style: String { "[Link](link)" } public func build(from text: String) -> String { "[(text)](https://github.com)"} }Copy the code

Struct MarkdownView: View is the main interface of the Demo and needs to store all supported Markdown Builders, as well as the currently selected Builder.

So, without thinking, we wrote the following code:

struct MarkdownView: View {
  private let allBuilders: [MarkdownBuilder]
  private var selectedBuilders: [MarkdownBuilder]
}
Copy the code

The result can be imagined!

How to do?

Generics? It doesn’t seem to work here!

Define all supported Builders one by one?

Too stupid! It does not comply with the OCP principle.

In this case, you need to use the main character of this section: Type Erasure.

Type Erasure is a generic technology that is not specific to Swift. The core idea is to erase (transform) existing types at compile time and make them invisible to business parties.

There are many ways to implement Type Erasure, such as Boxing, Closures, etc.

In MarkdownEditor, we use Boxing to implement Type Erasure, which is simply a Wrapper around an existing Type:

public struct AnyBuilder: MarkdownBuilder {

  public let style: String
  public var id: String { "AnyBuilder-(style)" }

  private let wrappedApply: (String) -> String

  public init<B: MarkdownBuilder>(_ builder: B) {
    style = builder.style
    wrappedApply = builder.build(from:)
  }

  public func build(from text: String) -> String {
    wrappedApply(text)
  }

  public static func == (lhs: AnyBuilder, rhs: AnyBuilder) -> Bool {
    lhs.id == rhs.id
  }
}
Copy the code

A few key points:

  • AnyBuilderTo achieve theMarkdownBuilderProtocol, (normally wrappers need to implement the protocol to be wrapped);
  • itsinitIs a generic method and passes in parametersstyle,build(from:)Store it down;
  • In its ownbuild(from:)Method to call stored directlywrappedApply, which itself is equivalent to a forwarding agent.

Also, extend MarkdownBulider:

public extension MarkdownBuilder {
  func asAnyBuilder() -> AnyBuilder {
    AnyBuilder(self)
  }
}
Copy the code

Now we can happily use AnyBuilder in MarkdownView:

struct MarkdownView: View {
  private let allBuilders: [AnyBuilder]  
  private var selectedBuilders: [AnyBuilder]
}
Copy the code

With the MarkdownBuilder extension above, instances of AnyBuilder can be generated in two ways:

  • BoldBuilder().asAnyBuilder()
  • AnyBuilder(BoldBuilder())

There are many types of Erasure implemented through Boxing in the Swift library, such as AnySequence, AnyHashable, AnyCancellable, and so on.

It’s almost always prefixed with Any.

Opaque Types


If we are going to make MarkdownEditor a standalone tripartite library, we are not going to expose any implementation details other than the MarkdownBuilder protocol to increase its flexibility.

That is, ItalicsBuilder, BoldBuilder, StrikethroughBuilder, and LinkBuilder are all library private.

How to do?

Again, without thinking, I wrote the following code:

public func italicsBuilder() -> MarkdownBuilder {
  ItalicsBuilder()
}

public func boldBuilder() -> MarkdownBuilder {
  BoldBuilder()
}

public func strikethroughBuilder() -> MarkdownBuilder {
  StrikethroughBuilder()
}

public func linkBuilder() -> MarkdownBuilder {
  LinkBuilder()
}
Copy the code

We want to create the appropriate Builder instance for the business side through public func and return it as an interface.

Ideal fullness, the reality of bone!

The same mistake awaits you!

How to do?

It’s time for Opaque Types!

Opaque Types allow functions/methods to return the protocol, not the type.

A function or method with an opaque return type hides its return value’s type information. Instead of providing a concrete type as the function’s return type, the return value is described in terms of the protocols it supports.

A few key points:

  • Keyword some. Add some before returning the protocol type, for example: Public func regularBuilder() -> some MarkdownBuilder instead of public func regularBuilder() -> MarkdownBuilder

  • The biggest differences between Opaque Types and returning the protocol type directly are:

    • Opaque Types only hide the type details from the user, which the compiler knows;

    • Returning the protocol type directly is runtime behavior and is unknown to the compiler.

    • In the following code, the compiler explicitly knows that the return type of the italicsBuilder method is italicsBuilder, but the method caller only knows that the return value complies with the MarkdownBuilder protocol. Thus hiding the implementation details;

      public func italicsBuilder() -> some MarkdownBuilder {
        ItalicsBuilder()
      }
      Copy the code
    • Because the compiler needs to explicitly determine the actual type behind Opaque Types, it is not allowed to return different Types in Opaque Types methods, such as the following:

      public func italicsBuilder() -> some MarkdownBuilder {
        if ... {
          return ItalicsBuilder()
        }
        else {
          return BoldBuilder()
        }
      }
      Copy the code

Well, now we know that we can simply add the keyword some to the code we wrote off the top of our head, without further ado.

Opaque Types are used extensively in SwiftUI. You could even say that Opaque Types were made for SwiftUI.

Phantom Types


Phantom Types themselves are not very relevant to this article, but as a similar concept, let’s briefly introduce them.

Phantom Types are also not specific to Swift and are a general coding trick.

Phantom Types are not strictly defined, but are generally described as appearing in generic parameters but not actually being used.

The Role in the following code (example from How to Use Phantom types in Swift) appears only in generic parameters and is not used in the Employee implementation:

struct Employee<Role>: Equatable {
    var name: String
}
Copy the code

What?

What is Phantom Types for?

Used to further strengthen the type.

Employee may have different roles, such as Sales, Programmer, etc., which we define as empty enum:

enum Sales { }
enum Programmer { }
Copy the code

Since Employee implements Equatable, it is possible to perform such operations between the two instances.

But judgment clearly only makes sense if it is performed between the same roles:

let john = Employee<Sales>.init(name: "John")
let sea = Employee<Programmer>.init(name: "Sea")

john == sea
Copy the code

Because Phantom Types are in action, the parse in the above code fails:

Cannot convert value of type ‘Employee’ to expected argument type ‘Employee’

Define Phantom Types as empty enum so that it cannot be instantiated to truly satisfy the Phantom Types semantics.

Since Swift does not have a keyword like name Acing, it is common to use empty enum to achieve a similar effect, as in Publishers in the Apple Combine Framework:

public enum Publishers {}
Copy the code

Then add a definition for the specific Publisher type in the Extension, such as:

extension Publishers { struct First<Upstream>: Publisher where Upstream: Publisher { ... }}Copy the code

Thus, specific Publishers can be referenced by way of Publishers.First.

The benefits of proper use of namespaces are described in Five Powerful, Yet Lesser-Known Ways to Use Swift Enums:

Using the above kind of namespacing can be a great way to add clear semantics to a group of types without having to manually attach a given prefix or suffix to each type’s name.

So while the above First type could instead have been named FirstPublisher and placed within the global scope, The current implementation makes it publicly available as Publishers.First — which both really reads nicely, and also gives us a hint that First is just one of many publishers available within the Publishers namespace.

It also lets us type Publishers. within Xcode to see a list of all available publisher variations as autocomplete suggestions.

summary


As the initiator of Protocol Oriented Programming (POP), Swift naturally plays an important role in Protocol, which is endowed with its powerful capabilities.

At the same time, Swift is Type safe, so there are certain restrictions on the use of protocols with Self requirements/Associated types.

With examples, this article mainly describes how to solve the above limitations through Type Erasure, Opaque Types, and Generics.

inOpaque Return Types and Type ErasureIn this article, the author analyzes whether they understand the privacy behind Protocols, Opaque Types, Generics and Type Erasure from the perspective of library developer (Liam), compiler (Corrine) and user (Abbie) respectively:As shown above:

  • Separate Protocols:

    • The protocol itself has the feature of hiding implementation details and runtime instantiation, so the compiler and user cannot know the real type behind it.
    • However, as the developer of the library (he wrote the code), I know exactly what the real types behind Protocol might be.
  • Opaque Types:

    • As with Protocols, the library developer must be aware;
    • The Opaque Types restriction can correspond to only one real type and must be specified during compilation, so the compiler knows it.
    • As far as the user is concerned, they still see the Protocol that hides the details.
  • Generics:

    • Generics cede type control to the user, so the library developer doesn’t know the real type, but the user does;
    • Generics are compile-time behavior, so the compiler knows exactly what the generic type is.
  • Type Erasure:

    • Type erasure is the behavior of the user, used to avoid compilation errors, etc., so only the user knows about it.

The resources

Swift-evolution · Opaque Result Types

OpaqueTypes

Different flavors of type erasure in Swift

Opaque Return Types and Type Erasure

Phantom types in Swift

How to use phantom types in Swift

Swift /TypeMetadata. RST at main · apple/swift · GitHub

Swift/typelayout. RST at main · apple/swift · GitHub

Swift Type Metadata

Understanding Swift Performance · WWDC2016

Swift.org – Whole-Module Optimization in Swift 3