Video #47: Predictable Randomness: Part 1
Episode: Video #47 Date: Feb 18, 2019 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep47-predictable-randomness-part-1

Description
Let’s set out to make the untestable testable. This week we make composable randomness compatible with Swift’s new APIs and explore various ways of controlling those APIs, both locally and globally.
Video
Cloudflare Stream video ID: 0499b9706541abdd70a76d983ecaa9da Local file: video_47_predictable-randomness-part-1.mp4 *(download with --video 47)*
References
- Discussions
- SE-0202: Random Unification
- Allow Error to conform to itself
- A Little Respect for AnySequence
- Type Erasure in Swift
- 0047-predictable-randomness-pt1
- Brandon Williams
- Stephen Celis
- Mastodon
- GitHub
- CC BY-NC-SA 4.0
- source code
- MIT License
Transcript
— 0:05
Previously on Point-Free we spent several episodes exploring the topic of randomness. It’s a topic that seems at odds with functional programming. We like to work with functions that take data in and spit data out in a consistent fashion, but randomness is all about inconsistency!
— 0:25
Despite this seemingly irreconcilable difference, we found that a randomness API deeply benefited from functional programming principles, specifically composition. We were able to distill the idea of randomness into a single type that could be transformed and combined using familiar functional concepts, and we ended up with a little library that was much more flexible and extensible than the randomness APIs that ship with Swift.
— 0:49
But even though we built this library around functional principles, it’s far from functional. We’d be remiss to ignore the fact that it’s completely unpredictable and untestable, and any code that uses it becomes immediately unpredictable and untestable as a result! One of the things we value in functional programming is the ability to reason about our code using predictable units that can be easily tested, so today we’re going to do something about it: we’re going to make the untestable testable. Gen recap
— 1:12
Let’s revisit the type we defined last time. It’s called Gen , is generic over some type A , and wraps a function that is responsible for generating random values in A when called. struct Gen<A> { let run: () -> A }
— 1:22
When we first defined this type, we were able to wrap Darwin’s arc4random function in a Gen value. import Darwin let random = Gen(run: arc4random) // Gen<UInt32>
— 1:28
And we could use this value to generate random, unsigned, 32-bit integers at will. random.run() // 3590175146 random.run() // 2820160976 random.run() // 934002752 random.run() // 1297868834 random.run() // 1360864610
— 1:36
The most interesting thing about Gen , though, was our ability to transform existing generators into new ones using function composition. All we needed to do was define map . extension Gen { func map<B>(_ f: @escaping (A) -> B) -> Gen<B> { return Gen<B> { f(self.run()) } } }
— 1:55
And transform existing generators we did! We took that single arc4random unit and showed that it was possible to recreate all of Swift’s randomness APIs.
— 2:05
In particular, we derived: let uniform = random.map { Double($0) / Double(UInt32.max) } func double(in range: ClosedRange<Double>) -> Gen<Double> { return uniform.map { t in t * (range.upperBound - range.lowerBound) + range.lowerBound } }
— 2:21
We could also generate random UInt64 values by running the random generator twice, once for the lower bits and once for the upper bits: let uint64 = Gen<UInt64> { let lower = UInt64(random.run()) let upper = UInt64(random.run()) << 32 return lower + upper }
— 2:29
And then from that we could generate random integers in a range. This is a little trickier to avoid modulo bias, but the work being done here is essentially what the standard library has to do to avoid the problem too: func int(in range: ClosedRange<Int>) -> Gen<Int> { return Gen<Int> { var delta = UInt64( truncatingIfNeeded: range.upperBound &- range.lowerBound ) if delta == UInt64.max { return Int(truncatingIfNeeded: uint64.run()) } delta += 1 let tmp = UInt64.max % delta + 1 let upperBound = tmp == delta ? 0 : tmp var random: UInt64 = 0 repeat { random = uint64.run() } while random < upperBound return Int( truncatingIfNeeded: UInt64( truncatingIfNeeded: range.lowerBound ) &+ random % delta ) } }
— 2:43
We also had random boolean generators: let bool = int(in: 0...1).map { $0 == 1 }
— 2:45
And generators for random elements of arrays: func element<A>(of xs: [A]) -> Gen<A?> { return int(in: 0...(xs.count - 1)).map { idx in guard !xs.isEmpty else { return nil } return xs[idx] } }
— 2:48
But then we were able to build brand new APIs that don’t ship with Swift, like the ability to generate randomly sized arrays of random values, in a very lightweight manner. extension Gen { func array(of count: Gen<Int>) -> Gen<[A]> { return count.map { count in var array: [A] = [] for _ in 1...count { array.append(self.run()) } return array } } }
— 3:10
It’s important to note that this is a function that takes a generator and it returns a generator. So if you give it a way of generating random integers, you will get back a way of generating random arrays whose lengths are determined by that random Int generator.
— 3:22
We finally saw that Gen had a zip operation that allows us to generalize map to work on functions with more than one argument, and we used that to zip up a bunch of generators into a single generator, usually our own data type structs. func zip<A, B>(_ ga: Gen<A>, _ gb: Gen<B>) -> Gen<(A, B)> { return Gen<(A, B)> { (ga.run(), gb.run()) } } func zip<A, B, C>( with f: @escaping (A, B) -> C ) -> (Gen<A>, Gen<B>) -> Gen<C> { return { zip($0, $1).map(f) } } Gen modernization and clean-up
— 3:38
While we were able to demonstrate how powerful Gen is by deriving all of these generators from a single unit, it’s not the most practical implementation: our dependence on arc4random prevents us from using Gen where it isn’t available, like on Linux. Let’s clean up what we have by migrating over to Swift’s cross-platform randomness APIs. Not only will this allow us to use Gen on any platform, it will lay the foundation for making Gen testable.
— 4:00
We can start by updating double(in:) to call Swift’s Double.random(in:) under the hood. func double(in range: ClosedRange<Double>) -> Gen<Double> { return Gen { Double.random(in: range) } }
— 4:08
And we can get rid of uniform in the process because it was only used to derive a random double in a range.
— 4:13
Next, let’s update int(in:) . func int(in range: ClosedRange<Int>) -> Gen<Int> { return Gen<Int> { Int.random(in: range) } }
— 4:22
Along with it we can get rid of uint64 . It a unit we used to define int(in:) , but it seems less useful on its own than a function that takes a range.
— 4:30
Rather than derive our bool generator from other generators, we can call Bool.random directly. let bool = Gen { Bool.random() }
— 4:35
And finally, we can update element(of:) to call randomElement . func element<A>(of xs: [A]) -> Gen<A?> { return Gen { xs.randomElement() } }
— 4:43
Gen and its units are now just wrappers around Swift’s randomness APIs, but Gen adds a more composable, transformable interface for randomness to the mix.
— 4:57
Our code is now fully platform-agnostic, and to get there we mostly ended up deleting some extra glue code.
— 5:10
While we’re in clean-up mode, there’s one last change I’d like to make. We’ve implemented these generators freely at the top level of the playground, but the natural place for these functions and values is as statics inside the Gen type itself. This is something we did a lot in our series of episodes on protocols witnesses ( part 1 , part 2 , part 3 , part 4 ).
— 5:28
For example, we can re-open Gen where its value is constrained to Double and redefine double(in:) there. extension Gen where A == Double { static func double(in range: ClosedRange<Double>) -> Gen { return Gen { Double.random(in: range) } } }
— 5:43
We can even use inference to make things super short. extension Gen where A == Double { static func double(in range: ClosedRange<Double>) -> Gen { return Gen { .random(in: range) } } }
— 5:49
And we can do the same by re-opening Gen where its value is constrained to Int and redefine int(in:) . The body looks identical to double(in:) . extension Gen where A == Int { static func int(in range: ClosedRange<Int>) -> Gen { return Gen { .random(in: range) } } }
— 5:56
Next, let’s move bool . We just promote it to a static let : extension Gen where A == Bool { static let bool = Gen { .random() } }
— 6:02
And what about element(of:) ? Well, we can move it into an unconstrained extension on Gen . extension Gen { static func element(of xs: [A]) -> Gen<A?> { return Gen<A?> { xs.randomElement() } } }
— 6:18
And array(of:) is already defined inside Gen as a method, so nothing to change here.
— 6:21
Before we wrap up, one last thing that would be nice to change is our int(in:) and double(in:) generators. They’re currently locked to the very specific Int and Double types, but Swift comes with a bunch of numeric types that all support the randomness API out of the box, so it’s unfortunate that we’ve lost that.
— 6:38
Instead of constraining directly to Int , we can constrain against any value that conforms to the FixedWidthInteger protocol. This includes Int64 , UInt64 , Int32 , UInt32 , and the rest. extension Gen where A: FixedWidthInteger { static func int(in range: ClosedRange<A>) -> Gen { return Gen { .random(in: range) } } }
— 6:57
Now we’ve recovered the ability to generate random UInt64 s, and we’ve expanded our ability to generate all of the other integer types Swift supports. Gen<UInt64>.int(in: range: .min ... .max) // Gen<UInt64>
— 7:08
And we can run it a few times. Gen<UInt64>.int(in: range: .min ... .max).run() Gen<UInt64>.int(in: range: .min ... .max).run() Gen<UInt64>.int(in: range: .min ... .max).run() Gen<UInt64>.int(in: range: .min ... .max).run() Gen<UInt64>.int(in: range: .min ... .max).run()
— 7:11
Generalizing double(in:) is similar. Swift provides randomness support for any floating point type that conforms to BinaryFloatingPoint with the additional requirement that one of its associated types. extension Gen where A: BinaryFloatingPoint { static func double(in range: ClosedRange<A>) -> Gen { return Gen { .random(in: range) } } } Reference to member ‘random’ is ambiguous
— 7:31
But there’s an additional requirement in this case.
— 7:33
If you didn’t know already, floating point numbers are represented in Swift by a specific binary format. They are of the form: // a * pow(2, n)
— 7:43
This a is called the significand and n is called the exponent.
— 7:47
For example: 12345.0.significand // 1.5069580078125 12345.0.exponent // 13
— 8:03
And if we fill them in: 1.5069580078125 * pow(2, 13) // 12345
— 8:15
If we hop over to the definition of BinaryFloatingPoint , we see that it has a couple of associated types for encoding the significand and the exponent public protocol BinaryFloatingPoint: ExpressibleByFloatLiteral, FloatingPoint { /// A type that represents the encoded significand of a value. associatedtype RawSignificand: UnsignedInteger /// A type that represents the encoded exponent of a value. associatedtype RawExponent: UnsignedInteger
— 8:24
In the case that a BinaryFloatingPoint ‘s significand conforms to FixedWidthInteger we get access to randomness API’s, which we can see by looking up the source: extension BinaryFloatingPoint where Self.RawSignificand: FixedWidthInteger { public static func random<T: RandomNumberGenerator>( in range: Range<Self>, using generator: inout T ) -> Self public static func random(in range: Range<Self>) -> Self public static func random<T: RandomNumberGenerator>( in range: ClosedRange<Self>, using generator: inout T ) -> Self public static func random(in range: ClosedRange<Self>) -> Self }
— 8:37
So, if we extend Gen under these conditions we will get access to the randomness APIs: extension Gen where A: BinaryFloatingPoint, A.RawSignificand: FixedWidthInteger { static func double(in range: ClosedRange<A>) -> Gen { return Gen { .random(in: range) } } }
— 8:46
Finally, let’s rename double to float since we’ve generalized it to all floating point types, like Float and CGFloat . extension Gen where A: BinaryFloatingPoint, A.RawSignificand: FixedWidthInteger { static func float(in range: ClosedRange<A>) -> Gen { return Gen { .random(in: range) } } }
— 8:56
And we take things for a spin by making a CGFloat generator. import UIKit Gen<CGFloat>.float(in: 0...1) // Gen<CGFloat>
— 9:19
And we can run it a few times. Gen<CGFloat>.float(in: 0...1).run() Gen<CGFloat>.float(in: 0...1).run() Gen<CGFloat>.float(in: 0...1).run() Gen<CGFloat>.float(in: 0...1).run() Gen<CGFloat>.float(in: 0...1).run() Taking Gen for another spin
— 9:26
Alright, things are looking good! We’ve got a very succinct, friendly library for composable, transformable randomness that utilizes Swift’s randomness APIs under the hood.
— 9:38
We also improved where we’ve housed these generators: as static values and functions on the Gen type.
— 9:47
Before we get to testing, let’s quickly use these generators to make something fun
— 10:00
Let’s start by creating a random UIColor . A color can be initialized with a red, green, blue and alpha value, all floats between 0 and 1. We could leverage Gen to pass random values to each field of the initializer. UIColor( red: Gen<CGFloat>.float(in: 0...1).run(), green: Gen<CGFloat>.float(in: 0...1).run(), blue: Gen<CGFloat>.float(in: 0...1).run(), alpha: Gen<CGFloat>.float(in: 0...1).run() )
— 10:37
So this generated a random UIColor , but we want a Gen of random UIColor s. Sounds like a job for zip , but we need a variant that takes four values: func zip4<A, B, C, D, Z>( with f: @escaping (A, B, C, D) -> Z ) -> (Gen<A>, Gen<B>, Gen<C>, Gen<D>) -> Gen<Z> { return { a, b, c, d in Gen<Z> { f(a.run(), b.run(), c.run(), d.run()) } } }
— 11:08
Now we can very easily create a random UIColor : let color = zip4(with: UIColor.init(red:green:blue:alpha:))( Gen<CGFloat>.float(in: 0...1), Gen<CGFloat>.float(in: 0...1), Gen<CGFloat>.float(in: 0...1), Gen<CGFloat>.float(in: 0...1) )
— 11:44
And we can run it a bunch of times. color.run() color.run() color.run() color.run() color.run()
— 11:48
We can also take advantage of the refactor we made earlier by using type inference in our zip. let color = zip4(with: UIColor.init(red:green:blue:alpha:))( .float(in: 0...1), .float(in: 0...1), .float(in: 0...1), .float(in: 0...1) )
— 11:59
Only problem is that I don’t want a random alpha, I want that to always be 1. let color = zip4(with: UIColor.init(red:green:blue:alpha:))( .float(in: 0...1), .float(in: 0...1), .float(in: 0...1), Gen<CGFloat> { 1 } )
— 12:13
This pattern is common enough that maybe we want to extract it out to its own helper. extension Gen { static func always(_ a: A) -> Gen<A> { return Gen { a } } }
— 12:37
And we can use this helper in building our random color generator: let color = zip4(with: UIColor.init(red:green:blue:alpha:))( .float(in: 0...1), .float(in: 0...1), .float(in: 0...1), .always(1) )
— 12:40
So now we can generate random colors to our heart’s content. Let’s step it up a bit and from this generator derive a random UIImage generator that simply draws that color into a 1⨉1 pixel: let pixel: Gen<UIImage> = color.map { color -> UIImage in let rect = CGRect(x: 0, y: 0, width: 1, height: 1) return UIGraphicsImageRenderer(bounds: rect).image { ctx in ctx.cgContext.setFillColor(color.cgColor) ctx.cgContext.fill(rect) } }
— 14:15
And just like that, we have a random pixel generator that we can run. pixel.run() pixel.run() pixel.run() pixel.run()
— 14:19
Now I want to stick this 1⨉1 pixel image into a randomly sized UIImageView so that we can render it into the live view of this playground and we can see what we’ve come up with. In order to do that we need to be able to create random CGRect s in order to size the view: let rect = zip4(with: CGRect.init(x:y:width:height:))( Gen<CGFloat>.always(0), .always(0), .float(in: 50...400), .float(in: 50...400) )
— 15:36
We can run it a few times. rect.run() // {x 0 y 0 w 367 h 346} rect.run() // {x 0 y 0 w 202 h 184} rect.run() // {x 0 y 0 w 85 h 365} rect.run() // {x 0 y 0 w 131 h 343}
— 15:41
And then with that we can create a random UIImageView with a random color drawn inside: let pixelImageView: Gen<UIImageView> = zip(rect, pixel) .map { rect, pixel in let imageView = UIImageView(image: pixel) imageView.bounds = rect return imageView }
— 16:12
And let’s take it for a spin: pixelImageView.run() // UIImageView pixelImageView.run() // UIImageView pixelImageView.run() // UIImageView pixelImageView.run() // UIImageView Controlling Swift’s randomness API
— 16:41
We could continue to build more and more complex generators of random values from these simple ones, but we still have a big problem to solve: they’re impossible to test! We can’t test the output of a generator because it’s unpredictably random. Even if a test passes it may very well fail next time.
— 17:12
In functional programming we like to push side effects to the boundaries of the code we write, but the Gen type bakes side effects right into its core units! It makes calls directly to non-deterministic, impossible-to-test random functions. Let’s explore how Swift allows us to control its randomness APIs.
— 17:38
All of Swift’s random functions can be called with a value that conforms to a standard library protocol called RandomNumberGenerator . Int.random(in: <#ClosedRange<Int>#>, using: &<#RandomNumberGenerator#>)
— 17:49
By default, Swift uses SystemRandomNumberGenerator , so these two calls are equivalent: var srng = SystemRandomNumberGenerator() Int.random(in: 1...10, using: &srng) Int.random(in: 1...10)
— 18:19
The using parameter is precisely what we need to control Swift’s randomness APIs to be predictable. We can define our own type that conforms to RandomNumberGenerator but isn’t random at all.
— 18:31
The RandomNumberGenerator protocol has a single requirement: a next function that returns a random UInt64 . public protocol RandomNumberGenerator { public mutating func next() -> UInt64 }
— 18:42
Let’s define a mock generator that returns a constant value instead. struct MockRandomNumberGenerator: RandomNumberGenerator { mutating func next() -> UInt64 return 42 } }
— 19:04
When we use it, we get a value. var mrng = MockRandomNumberGenerator() Int.random(in: 1...10, using: &mrng) // 3 Int.random(in: 1...10, using: &mrng) // 3 Int.random(in: 1...10, using: &mrng) // 3 Int.random(in: 1...10, using: &mrng) // 3 Int.random(in: 1...10, using: &mrng) // 3
— 19:19
We always get the same value!
— 19:23
Now that we’ve seen that we can control Swift’s randomness APIs, let’s take an approach that’s slightly more robust and flexible than our mock generator.
— 19:31
We’ll start by pasting in some code: a generator that we took from Swift’s open source test suite. struct LCRNG: RandomNumberGenerator { var seed: UInt64 init(seed: UInt64) { self.seed = seed } mutating func next() -> UInt64 { seed = 2862933555777941757 &* seed &+ 3037000493 return seed } }
— 19:40
This is what’s called a “linear congruential generator”: it uses an algorithm that yields a sequence of pseudorandom numbers from a given “seed” value. Given the same seed, it will produce the same series of numbers.
— 19:59
And we can test it out. Unlike our mock generator, we get a pleasantly random sequence. var lcrng = LCRNG(seed: 0) Int.random(in: 1...10, using: &lcrng) // 4 Int.random(in: 1...10, using: &lcrng) // 9 Int.random(in: 1...10, using: &lcrng) // 4 Int.random(in: 1...10, using: &lcrng) // 5 Int.random(in: 1...10, using: &lcrng) // 6
— 20:12
It’s looking pretty random, and we can confirm that the seed has mutated: lcrng.seed // 4803147403667321785
— 20:20
But we still have control over the randomness. Given the same seed, we’ll get the same sequence of values over time. lcrng.seed = 0 Int.random(in: 1...10, using: &lcrng) // 4 Int.random(in: 1...10, using: &lcrng) // 9 Int.random(in: 1...10, using: &lcrng) // 4 Int.random(in: 1...10, using: &lcrng) // 5 Int.random(in: 1...10, using: &lcrng) // 6
— 20:37
We should probably note that while this generator is perfect for writing tests or code that needs to be predictably random from a given seed value, you probably don’t want to use it as a replacement for the default SystemRandomNumberGenerator in most of your production code, especially if it’s security-sensitive. Controlling with Environment
— 21:03
It’s interesting that Swift’s randomness API is giving us a way to control randomness. It’s an admission that when you inject randomness into your code it injects uncertainty into everything it touches, making it completely impossible to test. They knew this needed to be solved, so they designed their API accordingly.
— 21:23
Although it is straightforward to control randomness in Swift’s API’s at the local level, Apple does not give us a way to control randomness across our entire application. If we are making these random(in:) calls all over our app, how will we be able to dynamically swap out different types of random number generators?
— 21:41
There’s one approach we could use, that actually goes back to our episodes on dependency injection ( “made easy” , “made comfortable” ) and the Environment struct for holding dependencies.
— 22:05
What if we had an environment that held our random number generator: struct Environment { var rng = SystemRandomNumberGenerator() } var Current = Environment()
— 22:31
Then we are free to use our “current” random number generator anytime we want some randomness: Int.random(in: 0...100, using: &Current.rng) // 81 Int.random(in: 0...100, using: &Current.rng) // 85 Int.random(in: 0...100, using: &Current.rng) // 13 Int.random(in: 0...100, using: &Current.rng) // 42
— 22:48
And this can be used everywhere, and would be pervasive in our application.
— 22:54
Then, the hope is that during tests we could swap in our LCRNG: Current.rng = LCRNG(seed: 0) Cannot assign value of type ‘LCRNG’ to type ‘SystemRandomNumberGenerator’ However, that can’t work because the environment’s RNG has a concrete type set to SystemRandonNumberGenerator .
— 23:26
We need to put a protocol in front of it so that we can swap in our LCRNG latter: struct Environment { var rng: RandomNumberGenerator = SystemRandomNumberGenerator() } var Current = Environment() Int.random(in: 0...100, using: &Current.rng) In argument type ‘inout RandomNumberGenerator’, ‘RandomNumberGenerator’ does not conform to expected type ‘RandomNumberGenerator’
— 23:46
Now we have an error to deal with. It’s a bit of a puzzling one, but we can take a look at the signature of random and it starts to make sense. public static func random<T: RandomNumberGenerator>( in range: ClosedRange<Int>, using generator: inout T ) -> Int
— 24:00
Swift’s API is generic over a type T that conforms to RandomNumberGenerator , but we defined our run function on Gen to work with the non-generic RandomNumberGenerator protocol directly. When we try to pass a value of the non-generic protocol, Swift complains that RandomNumberGenerator doesn’t conform to RandomNumberGenerator , which is true: protocols don’t conform to themselves. While this is a limitation that may be lifted in future versions of Swift, our current design just doesn’t play nicely with Swift’s randomness API.
— 24:42
We could introduce a generic to Environment to capture this. struct Environment<RNG: RandomNumberGenerator> { var rng: RNG = SystemRandomNumberGenerator() } Cannot convert value of type ‘SystemRandomNumberGenerator’ to specified type ‘RandomNumberGenerator’
— 25:04
The problem here is we’ve made Environment’s generator generic, but we’re still pinning its default directly to SystemRandomNumberGenerator . There may be a day where Swift introduces default generics, which could help us fix this, but for now we’ll have to move that work elsewhere, like by using the initializer. struct Environment<RNG: RandomNumberGenerator> { var rng: RNG } var Current = Environment(rng: SystemRandomNumberGenerator()) … Current.rng = LCRNG(seed: 0) Cannot convert value of type ‘LCRNG’ to specified type ‘SystemRandomNumberGenerator’
— 25:41
But really this doesn’t seem like an ideal way of controlling things on Environment . The type is currently locked to SystemRandomNumberGenerator at compile time, so if we want to control it, we’ll need to use a compiler condition here. struct Environment<RNG: RandomNumberGenerator> { var rng: RNG } #if RELEASE var Current = Environment(rng: SystemRandomNumberGenerator()) #else var Current = Environment(rng: LCRNG(seed: 0)) #endif
— 26:15
And now we’ve made it so we can control the generator based off of different compile-time variables.
— 26:29
Really, though, at this point the generic isn’t really pulling its weight. Why not move the compiler directive inside of Environment to get rid of this quirk. struct Environment { #if RELEASE var rng = SystemRandomNumberGenerator() #else var rng = LCRNG(seed: 0) #endif } var Current = Environment()
— 26:54
This solution is also not super ideal. We can’t swap the type of RandomNumberGenerator out easily like other controllable things. If you want to test a different random number generator (e.g., in your app or in a playground), you need to edit this conditional and recompile. But it’d be impossible to, in a single playground, reconfigure Current to see both the production and non-production version of something that uses Current.rng .
— 27:53
So the solution, although rigid, at least allows us to control randomness in our test environment and let it run free in production, but we can do one more thing to make things even nicer.
— 28:08
What we’re going to do is revisit an earlier error: In argument type ‘inout RandomNumberGenerator’, ‘RandomNumberGenerator’ does not conform to expected type ‘RandomNumberGenerator’
— 28:23
There’s a standard trick in Swift to solve the problem of protocols not conforming to themselves, and the RandomNumberGenerator protocol is simple enough to take advantage of this trick.
— 28:34
We can define a concrete data type that can wrap any other random number generator. We can call it AnyRandomNumberGenerator . struct AnyRandomNumberGenerator: RandomNumberGenerator { var rng: RandomNumberGenerator mutating func next() -> UInt64 { return self.rng.next() } }
— 29:16
And then Environment can hold onto one of these wrapper values instead. struct Environment<RNG: RandomNumberGenerator> { var rng = AnyRandomNumberGenerator( rng: SystemRandomNumberGenerator() ) }
— 29:40
Now we can reassign Current to use
LCRNG 29:50
And it all works in a dynamic, reassignable way! Int.random(in: 0...100, using: &Current.rng) // 54 Int.random(in: 0...100, using: &Current.rng) // 66 Int.random(in: 0...100, using: &Current.rng) // 43 Int.random(in: 0...100, using: &Current.rng) // 63 Current.rng = AnyRandomNumberGenerator(rng: LCRNG(seed: 0)) Int.random(in: 0...100, using: &Current.rng) // 82 Int.random(in: 0...100, using: &Current.rng) // 0 Int.random(in: 0...100, using: &Current.rng) // 28 Int.random(in: 0...100, using: &Current.rng) // 5 Where the first section is very random, and the second section is controlled.
LCRNG 30:14
Writing these kinds of wrappers is a pretty standard trick when dealing with some of Swift’s shortcomings around protocols. You can see it in the standard library with AnySequence and AnyHashable , and you may have encountered an AnyError type that wraps any error for contexts that require a type that conforms to Error , but you have an Error value itself, which, before Swift 5, didn’t conform to Error and needed to be wrapped.
LCRNG 30:51
This is a standard trick, but it’s interesting in our context. Swift provided a very local solution to solving randomness: when you call Int.random(in:using:) , you have a very specific input that can control the randomness. Swift gave no solution or guidance for controlling randomness globally across an entire application. If you sprinkle various random functions throughout your application, you need to control each one individually.
LCRNG 31:26
It turns out that the Environment struct is the perfect to control Swift’s APIs globally. Even if you didn’t want to use Gen , you could use Environment to control Swift’s APIs to be testable. To be continued…
LCRNG 31:46
Although we could probably do something nicer if we used Gen . Let’s see what it takes to control the Gen type… References SE-0202: Random Unification Alejandro Alonso • Sep 8, 2017 This Swift Evolution proposal to create a unified random API, and a secure random API for all platforms, was accepted and implemented in Swift 4.2. https://github.com/apple/swift-evolution/blob/master/proposals/0202-random-unification.md Composable Randomness Brandon Williams & Stephen Celis • Sep 17, 2018 The Gen type made its first appearance in this episode bridging function composition with randomness. Note Randomness is a topic that may not seem so functional, but it gives us a wonderful opportunity to explore composition. After a survey of what randomness looks like in Swift today, we’ll build a complex set of random APIs from just a single unit. https://www.pointfree.co/episodes/ep30-composable-randomness Dependency Injection Made Easy Brandon Williams & Stephen Celis • May 21, 2018 We first introduced the Environment concept for controlling dependencies in this episode. Note Today we’re going to control the world! Well, dependencies to the outside world, at least. We’ll define the “dependency injection” problem and show a lightweight solution that can be implemented in your code base with little work and no third party library. https://www.pointfree.co/episodes/ep16-dependency-injection-made-easy Allow Error to conform to itself John McCall • Dec 5, 2018 Swift 5.0 finally introduced the Result type to the standard library, and with it a patch that conforms Error to itself, allowing Result’s Failure parameter to be constrained to Error in an ergonomic fashion. While this conformance is a special case, Swift may automatically conform certain protocols to themselves in the future. https://github.com/apple/swift/pull/20629 A Little Respect for AnySequence Rob Napier • Aug 4, 2015 This blog post explores the need for AnySequence in Swift as a pattern for working around some of the shortcomings of protocols in Swift. http://robnapier.net/erasure Type Erasure in Swift Mike Ash • Dec 8, 2017 This edition of Friday Q&A shows how type erasure can manifest itself in many different ways. While you can wrap the functionality of a protocol in a concrete data type, as we explored in our series on protocol witnesses , you can also use subclasses and plain ole functions. https://www.mikeash.com/pyblog/friday-qa-2017-12-08-type-erasure-in-swift.html Downloads Sample code 0047-predictable-randomness-pt1 Point-Free A hub for advanced Swift programming. Brought to you by Brandon Williams and Stephen Celis . Content Become a member The Point-Free Way Beta previews Gifts Videos Collections Free clips Blog More About Us Community Slack Mastodon Twitter BlueSky GitHub Contact Us Privacy Policy © 2026 Point-Free, Inc. All rights are reserved for the videos and transcripts on this site. All other content is licensed under CC BY-NC-SA 4.0 , and the underlying source code to run this site is licensed under the MIT License .