OK, now that we’ve given a refresher on how protocols work in Swift, and showed one of their biggest limitations, what can we do to address that? It turns out that when you define a protocol in Swift and conform a type to that protocol, the compiler is doing something very special under the hood in order to track the relationship between those two things. We are going to give a precise definition to that construction, and we will even recreate it directly in Swift code. This will mean that we are going to do the work that the compiler could be doing for us for free, but by us taking the wheel for this we will get a ton of flexibility and composability out of it.
Let’s start slowly with the protocol we’ve just been talking about: Describable
. We are going to de-protocolize this by creating a generic struct, where the generic parameter represents the type conforming to the protocol, and the struct will have fields corresponding to the requirements of the protocol.
So, Describable
had one requirement that said that it could turn itself into a string. We will represent this as a generic struct that wraps a function from the generic parameter into string:
struct Describing<A> {
var describe: (A) -> String
}
Let’s put this next to the Describable
protocol so that you can see the similarities:
protocol Describable {
var description: String { get }
}
struct Describing<A> {
var describe: (A) -> String
}
From this type we create instances, which are called “witnesses” to the protocol conformance:
let compactWitness = Describing<PostgresConnInfo> { conn in
"""
PostgresConnInfo(\
database: "\(conn.database)", \
hostname: "\(conn.hostname)", \
password: "\(conn.password)", \
port: "\(conn.port)", \
user: "\(conn.user)"\
)
"""
}
You can use this witness directly by just invoking the description
field directly:
compactWitness.describe(localhostPostgres)
// PostgresConnInfo(database: "pointfreeco_development", hostname: "localhost", password: "", port: "5432", user: "pointfreeco")
We get the same output as before, but because our witnesses are just values, we can create as many of them as we want.
let prettyWitness = Describing<PostgressConnInfo> {
"""
PostgresConnInfo(
database: "\($0.database)",
hostname: "\($0.hostname)",
password: "\($0.password)",
port: "\($0.port)",
user: "\($0.user)"
)
"""
}
And we can use each witness the same way.
prettyWitness.description(localhostPostgres)
// PostgresConnInfo(
// database: "pointfreeco_development",
// hostname: "localhost",
// password: "",
// port: "5432",
// user: "pointfreeco"
// )
We can keep going and define an additional witness that prints connection strings.
let connectionWitness = Describing<PostgressConnInfo> {
"""
postgres://\($0.user):\($0.password)@\($0.hostname):\($0.port)/\
\($0.database)
"""
}
connectionWitness.description(localhostPostgres)
"postgres://pointfreeco:@localhost:5432/pointfreeco_development"