3

Generating Random Numbers Elegantly in Swift

 6 months ago
source link: https://www.swiftjectivec.com/swift-randomnumbergenerator/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Generating Random Numbers Elegantly in Swift

// Written by Jordan Morgan // Oct 24th, 2023 // Read it in about 2 minutes // RE: Swift

I love caffeine. Like, I love caffeine. Lately, I’ve been pecking away at a little pet project to log mine. Since I mostly consume espresso, my caffeine logging usually boils down to one of four choices. I log either a…

  • Single shot (~ 64mgs of caffeine)
  • Double shot (~ 128mgs of caffeine)
  • Triple shot (~ 192mgs of caffeine)
  • Quad shot (~ 256mgs of caffeine)

As such, I have this little guy:

enum EspressoShot: Int, CaseIterable {
	case single = 64, double = 128, triple = 192, quadShot = 256
}

While whipping up some testing data, I wanted a random value from those. Easy enough to support:

enum EspressoShot: Int, CaseIterable {
	case single = 64, double = 128, triple = 192, quadShot = 256
	
	static func randomShot() -> EspressoShot {
		return EspressoShot.allCases.randomElement() ?? .single
	}
}

Certainly, that works:

let randomShot: EspressoShot = EspressoShot.randomShot()

But, after browsing some docs, I saw that you can easily plug in Swift’s SystemRandomNumberGenerator for your own types. RandomNumberGenerator itself is a protocol, but you don’t have to manually adopt it most of the time. Take a look:

enum EspressoShot: Int, CaseIterable {
	case single = 64, double = 128, triple = 192, quadShot = 256
	
	static func random<G: RandomNumberGenerator>(using generator: inout G) -> EspressoShot {
		return EspressoShot.allCases.randomElement(using: &generator)!
	}
	
	static func random() -> EspressoShot {
		var g = SystemRandomNumberGenerator()
		return EspressoShot.random(using: &g)
	}
}

The first function supports passing in any random number generator that adopts the bespoke protocol. However, the bottom one is super useful - we can simply use the built-in random number generator, and then pass it to the previous function to get our random value:

let randomShot: EspressoShot = EspressoShot.random()

Beautiful. Here’s why I like this:

  • The call site is a bit leaner.
  • It’s also more familiar, as Type.random() is a common Swift convention.
  • We don’t have to deal with force unwraps at the call site. It’s hidden.
  • It also supports using any random number generator, while our hand rolled one could not.
  • I’ll always use Swift’s implementation whenever possible. Their random number generator is automatically seeded, thread-safe and uses the appropriate APIs depending on the platform Swift is running (i.e. Windows (BCryptGenRandom), Linux (getrandom(2)) or Apple (arc4random_buf(3))).

It’s de rigueur for Swift codebases to be, well….Swifty, is it not? And this feels a bit more Swifty to me.

Update: The open source maestro himself, Sindre Sorhus, offered a nice solution wherein you can make any type conforming to CaseIterable have these capabilities:

extension CaseIterable {
	public static func randomCaseIterableElement(using generator: inout some RandomNumberGenerator) -> Self {
		allCases.randomElement(using: &generator)!
	}

	public static func randomCaseIterableElement() -> Self {
		var generator = SystemRandomNumberGenerator()
		return randomCaseIterableElement(using: &generator)
	}
}

enum Foo: String, CaseIterable {
	case a
	case b
	case c
}

print(Foo.randomCaseIterableElement())

Check out his gist here.

Until next time ✌️.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK