Seeded Random Number Generation, part 1

Randomness is relatively easy to come by in Swift. Most types you might want to randomize support the method random(in:), which takes a closed range and returns a value within the range. But there are use cases where you don’t just want randomness, but predictable randomness. This is a concept called seeded randomness, where you provide a seed (in the form of a string, a number, or some bytes) to the data structure that provides your randomness. That seed ensures that all data structures will return the same string of randomness. Predictable randomness! This needs unpacking.

The Swift protocol

Swift provides the protocol RandomNumberGenerator that requires implementation of one method:

func next() -> UInt64

This is the protocol we’ll focus our discussion on, with the following expansion:

protocol SeededRandomNumberGenerator: RandomNumberGenerator {
    typealias Seed = UInt64

    var seed: Seed { get }

    init(seed: Seed)
}

We will enforce an initializer that takes a seed in the form of UInt64 and we expose it for examination. The rest of our discussion will focus on the implementation of the next method in this protocol.

The use case

Generally, the reason you’re reaching for a random number is that you don’t want predictability, but there are use cases where you want the impression of randomness without the problems that actual randomness introduces.

Perhaps the most universal reason to reach for seeded randomness is testing. While in production, randomness may be essential to proper function of your app or service, but in testing determinism is paramount. Instead of reaching for random(in:), you can inject any RandomNumberGenerator into your class. In production code, you can use the SystemRandomNumberGenerator provided by Swift. In test, you can provide a SeededRandomNumberGenerator to provide determinism.

What got me interested in seeded random number generation is its use in games. It’s common for terrain in a game to be randomly generated as players explore. How do you ensure that two players on the same map generate the same random terrain? Seeded random number generation! How do you allow players to share maps and starting points with each other? You can generate and share the entire map, or you can implement seeding and just share the seed.

Our first attempt

We’ll call this our incremental random.

///  It would be challenging to come up with a *more basic* generator.
struct IncrementalRandom: SeededRandomNumberGenerator {
    typealias Seed = UInt64

    private var state: Seed

    public let seed: Seed

    init(seed: Seed) {
        self.seed = seed
        self.state = seed
    }

    mutating func next() -> UInt64 {
        state &+= 1
        return state
    }
}

It accepts a seed, then when next is called it increments the state by one and returns that value. When it reaches the maximum value of a UInt64 it wraps around and begins at zero. While it may fail our intuitive understanding of a random number generator, it actually passes a surprising number of tests for a random number generator, and we’ll start our discussion with those.

Determinism

The most important quality of a seeded random number generator is that for a given seed, the values returned are the the same every time and in the same order. For IncrementalRandom, it’s easy to see that for a seed x, the returned values will be x+1, x+2, and so on. This is the core competency of the seeded random number generator.

Periodicity

Periodicity refers to the number of calls to next required until the random number sequence repeats. Generally, the goal is to maximize the size of the period. For UInt64, our period can be as large as the max value of UInt64. We can also validate this pretty intuitively for our IncrementalRandom seeded generator… It won’t repeat a value until it passes through the entire space of UInt64 and returns to our seed. This is as good as it gets for periodicity, and so far it seems like our simple implementation is doing kind of well, but that won’t last. We’ll start to examine its shortcomings next.

Predictability

Intuitively, I’m sure you can already tell that our random number generator fails at this aspect of its task… A seeded random number generator is never truly random, but the numbers need to feel random. For IncrementalRandom, this failure manifests itself when you can tell for any given random value what the next in the sequence is. When the predictability is this obvious, it’s easy to understand the algorithm’s shortcoming, but as we increase in complexity, what we mean by predictability will get harder to intuit.

As an metric, we want predictability to be low. In a game, this is what will make a river’s flow look natural, and prevent mountain ranges from having peaks that decrease in height from east to west. In a non-game scenario, if you’re using a random number generator to assign work tasks to agents, predictability (as manifested by correlation) coould be seen in the system as some agents getting assigned more or fewer tasks than others, or some agents often getting assigned tasks after others. Whether or not this is a big deal relates to how important true randomness is to your app.

To be continued

We can do much better than IncrementalRandom, but it was a useful way to introduce topics in a way that can be intuited. We will look deeper at these concepts in part 2.


The Swift AWS Lambda Runtime

When we talk about Lambda runtimes and running Swift code in Lambda, it helps to start by understanding what a traditional Lambda implementation looks like. Selecting a provided runtime provides a scaffolding for your code to hook into that does the heavy lifting required for Lambda to function.

When providing a Java Lambda implementation, for example, you write a class that is typed to the Request and Response that your architecture will provide to and expect from the Lambda function respectively.

import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;

/**
 * Lambda handler written in Java that receives HTTP requests and returns a response.
 */
public class HttpHandler implements RequestHandler<Request, Response> {
  // perform logic on the request and return response
}

The only code you write is the class that implements RequestHandler and any supporting code that object needs to function. The Java Lambda runtime provides all the networking code and object serialization required to handle requests. Under the hood, the runtime is making calls to Lambda APIs to poll for events; behavior that is abstracted away by the runtime code. But if you don’t want to write Java Lambdas…

The Alternative

While reading the list of provided runtimes linked above, two stand out: the OS only runtimes. To deploy a Swift lambda, you must use the AL2 runtime and perform all the tasks that were previously abstracted away. AWS doesn’t provide the code needed to interface with the lambda service, but provides documentation for the APIs that allow you to do the heavy lifting yourself.

You would of course need to write an HttpHandler in Swift and any supporting code for that object, but you are also now responsible for the networking code and object serialization required to wire up that class with the Lambda service itself. This is what the Swift AWS Lambda Runtime provides. It is a project that bridges that gap and makes Swift lambdas practical.

In contrast with the Java lambda, we are providing a main method for the runtime, which was notably absent in the Java example. Our code is responsible for the entire workflow and is loaded directly when the OS only runtime boots!

import AWSLambdaRuntime

/// Lambda handler written in Swift that receives HTTP requests and returns a response.
@main
struct HttpHandler: LambdaHandler {

  func handle(_ event: Request, context: LambdaContext) async throws -> Response {
    // perform logic on the request and return response
  }
}

The elegance of the Swift AWS Lambda Runtime is encapsulated in the @main annotation on the LambdaHandler.

A Beautiful Abstraction

Aside from having to learn slightly more about the inner workings of a Lambda runtime, once you import and use the Swift AWS Lambda Runtime in your package there is not a significant practical difference in what you’re coding. The runtime is provided by the implementation of main that the Lambda Runtime package provides. When your class implements SimpleLambdaHandler or LambdaHandler, an extension on those classes provides a main that calls the Lambda runtime loop that integrates your lambda code.

It’s a very clean abstraction for a huge amount of boilerplate code that you would otherwise have to write. When compared to the AWS provided runtime implementations, the work required for a Swift developer is nearly identical despite the increased complexity behind the scenes.

Diving Deeper

To explore Swift on Lambda, I wrote a small project that uses the Swift AWS Lambda runtime along with CDK to build and deploy multiple Lambda functions that back an API Gateway endpoint. The functions are built as separate products using Swift Package Manager while sharing common code provided by a third SPM package in the same repository. You can view the source code on GitHub.