I was recently writing a test runner for the Cryptopals Crypto Challenges, and I ran into a problem. I had isolated all of my individual challenges into their own type, but I wanted to have a means of running all of my solutions at once. I naively wrote something like this:

let allChallenges: [Challenge] = [
    Challenge01(),
    Challenge02(),
    Challenge03(),
    Challenge04(),
    ...
]

func run(challenges: [Challenge]) {
    challenges.forEach { challenge in
        challenge.run()
    }
}

run(challenges: allChallenges)

This is good code. It does what I wanted. However, a number of these challenges use a good amount of memory when they’re initialized, and this approach pays the memory cost of initializing all of the challenges at once. I really only need these types to live for a short amount of time – how could I write this so it only initializes these objects right before using them?

Metaprogramming to the rescue

Metaprogramming is a big word for writing code that refers to the types in your code rather than the actual instances of those types. Fortunately, Swift supports this style of metaprogramming. Let’s see how we can use it.

First, some context. This was my Challenge protocol that all ChallengeXX types conformed to:

protocol Challenge {
    func run()
}

That worked when I didn’t have to dynamically create instances of those types, but now I need a way to do that. So let’s tweak that a bit:

protocol Challenge {
    init()
    func run()
}

That’s better. Now anything that conforms to the Challenge protocol can be instantiated with an init call, and then can be run by calling its run() method.

Using .Type and .self

Now that I can dynamically initialize and run these types, I need a strongly-typed way to describe that I’m referring to types – not instances – that conform to this protocol. Fortunately, each type in Swift has a .Type static property that refers to… well, to the type of that type. It’s confusing and hard to talk about. Let’s look at an example

let instanceOfChallenge: Challenge = Challenge01()

let typeOfChallenge: Challenge.Type = Challenge01.self

In the above, instanceOfChallenge is the problem I had before: it’s a concrete instance of a type that conforms to the Challenge protocol. Meanwhile, typeOfChallenge refers to the type of something that conforms to the Challenge protocol. It’s not instantiating anything in that case, just referring to the type of Challenge01.

And, notably, all types know how to refer to themselves with .self.

Let’s fix our code

First, the declaration of allChallenges. Rather than initialize all my challenges at once, let’s make that a list of the types of challenges that could be initialized.

let allChallenges: [Challenge.Type] = [
    Challenge01.self,
    Challenge02.self,
    Challenge03.self,
    Challenge04.self,
    ...
]

Great! We’ve solved our first problem; we’re no longer using a ton of memory when allChallenges is created. Next, let’s change our run(challenges:) method to support challenge types instead of challenge instances.

func run(challenges: [Challenge.Type]) {
    challenges.forEach { challengeType in
        let challenge = challengeType.init()
        challenge.run()
    }
}

A few things have changed here:

  • The type of our challenges parameter is now [Challenge.Type] instead of [Challenge]
  • We have to instantiate that type before running it now, so we had to add let challenge = challengeType.init()

Why challengeType.init() and not challengeType()? The type-followed-by-parenthesis (like String()) is syntactic sugar for explicitly calling the init method (String.init()). However, this syntactic sugar isn’t extended to metatypes, and the compiler will complain.

Error: Initializing from a metatype value must reference init explicitly

Once that’s all done, our call to run the challenge remains the same:

run(challenges: allChallenges)

Recap

Hopefully y’all found this useful. Metaprogramming like this is not a tool I reach for often, but it was absolutely the right tool to solve this problem. Now I can continue running all my crypto challenge solutions without using more memory than I need.