i’ve got a bunch of stats counters that look like
extension Stats
{
@frozen public
struct Decl:Equatable, Sendable
{
/// Typealiases.
public
var typealiases:Int
/// Structs and enums.
public
var structures:Int
/// Protocols.
public
var protocols:Int
/// Classes, excluding actors.
public
var classes:Int
/// Actors.
public
var actors:Int
...
after brainstorming some alternatives i eventually settled on a protocol-based solution that involves a protocol StatsCollection
with a bunch of static requirements:
protocol StatsCollection
{
static
var keys:[KeyPath<Self, Int>] { get }
static
func id(_ key:KeyPath<Self, Int>) -> String?
static
func display(_ key:KeyPath<Self, Int>) -> String?
}
a conformance looks like:
extension Stats.Decl:StatsCollection
{
static
var keys:[KeyPath<Self, Int>]
{
[
\.functions,
\.operators,
\.constructors,
\.methods,
\.subscripts,
\.functors,
\.protocols,
\.requirements,
\.witnesses,
\.attachedMacros,
\.freestandingMacros,
\.structures,
\.classes,
\.actors,
\.typealiases
]
}
static
func id(_ key:KeyPath<Self, Int>) -> String?
{
switch key
{
case \.functions: "decl function"
case \.operators: "decl operator"
case \.constructors: "decl constructor"
case \.methods: "decl method"
case \.subscripts: "decl subscript"
case \.functors: "decl functor"
case \.protocols: "decl protocol"
case \.requirements: "decl requirement"
case \.witnesses: "decl witness"
case \.attachedMacros: "decl macro attached"
case \.freestandingMacros: "decl macro freestanding"
case \.structures: "decl structure"
case \.classes: "decl class"
case \.actors: "decl actor"
case \.typealiases: "decl typealias"
case _: nil
}
}
static
func display(_ key:KeyPath<Self, Int>) -> String?
{
switch key
{
case \.functions: "global functions or variables"
case \.operators: "operators"
case \.constructors: "initializers, type members, or enum cases"
case \.methods: "instance members"
case \.subscripts: "instance subscripts"
case \.functors: "functors"
case \.protocols: "protocols"
case \.requirements: "protocol requirements"
case \.witnesses: "default implementations"
case \.attachedMacros: "attached macros"
case \.freestandingMacros: "freestanding macros"
case \.structures: "structures"
case \.classes: "classes"
case \.actors: "actors"
case \.typealiases: "typealiases"
case _: nil
}
}
}
i decided to use a protocol and not a macro because the Stats.Decl
structure is a fundamental data type that’s part of the database schema, while StatsCollection
is part of the rendering logic, and i didn’t want to include any rendering logic in the database schema module.
but i find KeyPath
s to be an awkward abstraction because they don’t have any concept of exhaustivity, so the mapping functions need to have a case _: nil
default clause. this means when i add new fields to the structure, there are a lot of disparate code locations to update and the compiler doesn’t provide a lot of guardrails here.
can we do better than KeyPath
?