Video #48: Predictable Randomness: Part 2
Episode: Video #48 Date: Feb 25, 2019 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep48-predictable-randomness-part-2

Description
This week we finally make our untestable Gen type testable. We’ll compare several different ways of controlling Gen, consider how they affect Gen’s API, and find ourselves face-to-face with yet another flatMap.
Video
Cloudflare Stream video ID: c268927c9bbcc87fcae9a23d218c4cac Local file: video_48_predictable-randomness-part-2.mp4 *(download with --video 48)*
References
- Discussions
- SE-0202: Random Unification
- The State Monad: A Tutorial for the Confused?
- Haskell/Understanding monads/State
- 0048-predictable-randomness-pt2
- Brandon Williams
- Stephen Celis
- Mastodon
- GitHub
- CC BY-NC-SA 4.0
- source code
- MIT License
Transcript
— 0:05
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.
— 0:42
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.
— 1:17
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.
— 1:37
We could probably do something even nicer if we used Gen , but for the moment it’s still completely uncontrollable. Maybe we can take control of Gen in a way similar to how we took control of Swift’s APIs. Controlling the Gen type
— 1:56
Now that we’ve seen that it’s possible to control Swift’s randomness APIs in a deterministic way, we need to update Gen accordingly. Gen values currently call out to random functions that don’t specify a RandomNumberGenerator .
— 2:08
For example, our int generator merely calls random with a range: extension Gen where A: FixedWidthInteger { func int(in range: ClosedRange<A>) -> Gen { return Gen { .random(in: range) } } }
— 2:15
We want to be able to pass a RandomNumberGenerator to the using parameter, but we don’t have one at our disposal. extension Gen where A: FixedWidthInteger { static func int(in range: ClosedRange<A>) -> Gen { return Gen { .random(in: range, using: ???) } } } Controlling with a global
— 2:23
Well, we could control Gen the same way we controlled Swift’s APIs by using Environment . extension Gen where A: FixedWidthInteger { static func int(in range: ClosedRange<A>) -> Gen { return Gen { .random(in: range, using: &Current.rng) } } }
— 2:32
But Current is a user-land solution and Gen and these basic building blocks are library-level concerns. We should be able to ship Gen as its own library so that we don’t have to redefine it in every app we build.
— 2:47
Maybe the library could provide a similar global as configuration that can be swapped out. var globalRng = AnyRandomNumberGenerator( rng: SystemRandomNumberGenerator() )
— 3:06
And all of our generators that depend on Apple’s APIs could use this global instead. extension Gen where A: FixedWidthInteger { static func int(in range: ClosedRange<A>) -> Gen { return Gen { .random(in: range, using: &globalRng) } } }
— 3:13
And now we have a way of controlling things at the library level that is separate from the concerns of your current application.
— 3:20
We can swap it out for an
LCRNG 3:45
While a global makes sense for user-land code, it’s a bit of an odd choice for a library like Gen . A single application may want to use multiple RandomNumberGenerator s, but this API prevents us from doing so safely. For example, a game may use SystemRandomNumberGenerator for effects and a seedable generator for mazes and random levels. Controlling with an argument
LCRNG 4:29
To control things more locally, we need to provide a RandomNumberGenerator as an explicit dependency.
LCRNG 4:37
For example, we can add an argument to the int(in:) function: extension Gen where A: FixedWidthInteger { static func int( in range: ClosedRange<A>, using rng: inout RandomNumberGenerator ) -> Gen { return Gen { .random(in: range, using: &rng) } } } Escaping closures can only capture inout parameters explicitly by value
LCRNG 5:01
But we get an error. We can’t capture inout parameters: these values can only be mutated for the lifecycle of the inout function. The Gen initializer is @escaping and captures this inout parameter, so we need to drop inout and make an explicit copy instead. extension Gen where A: FixedWidthInteger { static func int( in range: ClosedRange<A>, using rng: RandomNumberGenerator ) -> Gen { return Gen { var rng = rng return .random(in: range, using: &rng) } } } Ambiguous reference to member ‘random’
LCRNG 5:38
To get a better error message, let’s fully-qualify our return value with A . return A.random(in: range, using: &rng) Protocol type ‘RandomNumberGenerator’ cannot conform to ‘RandomNumberGenerator’ because only concrete types can conform to protocols
LCRNG 5:50
Oh right, we need to make this function generic to avoid the problem we solved last time . extension Gen where A: FixedWidthInteger { static func int<RNG: RandomNumberGenerator>( in range: ClosedRange<A>, using rng: RNG ) -> Gen { return Gen { var rng = rng return .random(in: range, using: & rng) } } }
LCRNG 6:21
And now we have something that compiles, but to control it we need to update our call sites to use this using parameter. Gen<UInt64>.int(in: .min ... .max, using: &srng).run() ‘&’ used with non-inout argument of type ‘SystemRandomNumberGenerator’
LCRNG 6:38
Oh right, we’ve changed the interface slightly from Swift’s to no longer take an inout parameter, so let’s drop the & . Gen<UInt64>.int(in: .min ... .max, using: srng).run()
LCRNG 6:47
Now we’ve built an interface that allows us to locally control which RandomNumberGenerator is being used by a Gen value.
LCRNG 6:57
Unfortunately, this solution isn’t a great one. We’ve made int(in:) controllable, but we’d need to change double(in:) , bool , and every other generator that calls Swift’s APIs under the hood to take a RandomNumberGenerator as well.
LCRNG 7:10
Unfortunately, this solution isn’t a great one. We’ve made int(in:) controllable, but at the cost of some extra boilerplate. Swift’s APIs all come with an overload that doesn’t require the using parameter, which can make them nicer to use when you don’t care about controlling things. But not the user of the Gen library must pass in an explicit RandomNumberGenerator . While could arguably be a good thing, a user may only care about Gen ‘s nice compositional and transformable properties. To restore that, we’d need to define an overload, as well. extension Gen where A: FixedWidthInteger { static func int(in range: ClosedRange<A>) -> Gen { let rng = SystemRandomNumberGenerator() return int(in: range, using: rng) } }
LCRNG 8:01
This gives us the same, ergonomic default that Swift’s APIs provide
LCRNG 8:06
Note that we couldn’t have provided a default value to the original function because the generic can be a different type. extension Gen where A: FixedWidthInteger { static func int<RNG: RandomNumberGenerator>( in range: ClosedRange<A>, using rng: RNG = SystemRandomNumberGenerator() ) -> Gen { return Gen { var rng = rng return .random(in: range, using: &rng) } } }
LCRNG 8:19
This extra API surface area spreads to every single generator that we want to control, from int(in:) to double(in:) to bool , and to every generator derived from these base units.
LCRNG 8:31
And all of these decisions just mean that the Gen type has lost the composability and lightweightedness that we had come to enjoy.
LCRNG 8:39
For example, if we wanted a controlled generator: let numbers = Gen<Int>.int(in: 1...10, using: lcrng) // Gen<Int> And later want to map it to its string representation: let stringNumbers = numbers.map(String.init) // Gen<String>
LCRNG 9:05
We may have derived this generator somewhere else in the codebase, but we have no way to change the random number generator that has already been bound to the original numbers value.
LCRNG 9:16
To do that we need to create wrapper functions for each generator: we need a wrapper function around numbers that takes a generic RandomNumberGenerator as input. And we need a wrapper function around stringNumbers that takes a generic RandomNumberGenerator and threads it to the numbers wrapper function. And these wrapper functions are necessary for every further composition we may have.
LCRNG 9:35
At the time of creation of our generators we have to make a choice about how we control randomness. We can no longer freely compose and derive new generators from old ones, as we did with UIColor , CGRect , and UIImageView , without locking ourselves into a method of randomness. And that seems overly restrictive! If we want to make any of these individual Gen values controllable, we need to wrapper functions that allow other RandomNumberGenerator s to be passed in at creation time.
LCRNG 10:05
There’s another problem with this solution that should let us know that it’s probably not the best way to move forward: we’ve removed that inout annotation from using , and that is a very strong thing to do. The signature does not advertise that the value passed in may be mutated, but secretly, under the hood, we’re making a copy so that we can mutate it. This means if you were to pass it a reference value, you wouldn’t know that the reference is going to be mutated, even though there’s nothing in the signature to indicate this. One of the nice things about inout is that it publicly advertises mutation.
LCRNG 10:43
Swift’s APIs also very purposefully use inout , and now we’re creating a subtly different API by dropping this requirement, and it doesn’t seem like the right thing to do. Controlling in the type
LCRNG 10:55
Enough false starts! Let’s take a different approach by observing the fact that our functional requirement is to take a RandomNumberGenerator up front but how we only care about this when we call run . Maybe we can instead bake this requirement into the Gen type itself so that we can retain all of the nice, lightweight compositional properties from before.
LCRNG 11:38
Let’s update the run function to take a generator as input: rather than go from () and pluck a value out of thin air, it will be given the ability to take a RandomNumberGenerator and work with it. This will ensure that we’ll have a generator to pass to Swift’s APIs. struct Gen<A> { let run: (RandomNumberGenerator) -> A }
LCRNG 12:04
We get a lot of compiler errors, so let’s try fixing them, one by one.
LCRNG 12:15
Now we need to fix map . Gen now takes a random number generator as an argument, so we can pass that along to self.run : extension Gen { func map<B>(_ f: @escaping (A) -> B) -> Gen<B> { return Gen<B> { rng in f(self.run(rng)) } } }
LCRNG 12:31
Now we have another compiler error for float(in:) . We can pass rng along: extension Gen where A: BinaryFloatingPoint, A.RawSignificand: FixedWidthInteger { static func float(in range: ClosedRange<A>) -> Gen { return Gen { rng in .random(in: range, using: &rng) } } } Reference to member FIXME
LCRNG 12:44
The problem here is that rng immutable, so, maybe we need to solve this problem as we did before, with a copy. extension Gen where A: BinaryFloatingPoint, A.RawSignificand: FixedWidthInteger { static func float(in range: ClosedRange<A>) -> Gen { return Gen { rng in var rng = rng return A.random(in: range, using: &rng) } } } In argument type ‘inout (RandomNumberGenerator)’, ‘RandomNumberGenerator’ does not conform to expected type ‘RandomNumberGenerator’
LCRNG 12:55
Ah, this is the same error as before. We need a type generic over RandomNumberGenerator . So how can we introduce a generic to play nicely with Apple’s APIs?
LCRNG 13:04
When we talked about Environment we thought about introducing another generic at the type-level. struct Gen<A, RNG: RandomNumberGenerator> { let run: (RNG) -> A } When we were working with static functions, we thought about introducing a generic at that scope, but we can’t introduce a generic to the run property directly. struct Gen<A> { let run: <RNG: RandomNumberGenerator>(RNG) -> A } This just isn’t possible in Swift right now.
LCRNG 13:18
Really the solution is to use the AnyRandomNumberGenerator so that we can later pass in concrete implementations of the protocol later. struct Gen<A> { let run: (AnyRandomNumberGenerator) -> A }
LCRNG 13:40
And now float(in:) is compiling. extension Gen where A: BinaryFloatingPoint, A.RawSignificand: FixedWidthInteger { static func float(in range: ClosedRange<A>) -> Gen { return Gen { rng in var rng = rng return .random(in: range, using: &rng) } } }
LCRNG 13:44
The copy we’re making is still unfortunate. We’re kind of hiding the inout logic away again, and value types and reference types are handled in subtly different ways, so let’s make it explicit in the Gen type. struct Gen<A> { let run: (inout AnyRandomNumberGenerator) -> A }
LCRNG 14:21
We now need to update map to use inout syntax with an & . extension Gen { func map<B>(_ f: @escaping (A) -> B) -> Gen<B> { return Gen<B> { rng in f(self.run(&rng)) } } }
LCRNG 14:27
And we can clean up `float(in:) by getting rid of the copy. extension Gen where A: BinaryFloatingPoint, A.RawSignificand: FixedWidthInteger { static func float(in range: ClosedRange<A>) -> Gen { return Gen { rng in .random(in: range, using: &rng) } } }
LCRNG 14:35
Now we have some errors because we’re calling float(in:) without passing it a mutable RNG. Gen<CGFloat>.float(in: 0...1).run() Missing argument for parameter #1 in call So let’s pass the SystemRandomNumberGenerator along. var srng = SystemRandomNumberGenerator() Gen<CGFloat>.float(in: 0...1).run(&srng) Cannot convert value of type ‘SystemRandomNumberGenerator’ to expected argument type ‘AnyRandomNumberGenerator’
LCRNG 15:00
Oh, but we need to wrap it up in an AnyRandomNumberGenerator . var srng = AnyRandomNumberGenerator( rng: SystemRandomNumberGenerator() ) Gen<CGFloat>.float(in: 0...1).run(&srng) That fixes that, so what’s next?
LCRNG 15:21
Alright, now we’ve come to some APIs that we tried to make work with the RandomNumberGenerator protocol and ended up with something clunky: extension Gen where A: FixedWidthInteger { static func int(in range: ClosedRange<A>) -> Gen { let rng = SystemRandomNumberGenerator() return int(in: range, using: rng) } static func int<RNG: RandomNumberGenerator>( in range: ClosedRange<A>, using rng: RNG = SystemRandomNumberGenerator() ) -> Gen { return Gen { var rng = rng return .random(in: range, using: &rng) } } }
LCRNG 15:33
Now we can fix things with a single, simple interface: extension Gen where A: FixedWidthInteger { static func int(in range: ClosedRange<A>) -> Gen { return Gen { rng in .random(in: range, using: &rng) } } }
LCRNG 15:58
We’ve reduced our API surface area because we no longer have to worry about overloads per generator function.
LCRNG 16:12
Now we can fix a bunch of compiler errors from our earlier attempts. Gen<UInt64>.int(in: .min ... .max, using: srng).run() Extra argument ‘using’ in call We no longer need to pass to using when creating a Gen value: we merely pass an AnyRandomNumberGenerator when we call run . Gen<UInt64>.int(in: .min ... .max).run(&srng)
LCRNG 16:40
For Bool we can pass the rng to Swift’s using API`. extension Gen where A == Bool { static let bool = Gen { rng in .random(using: &rng) } }
LCRNG 16:50
Same goes for plucking a random element from an array. extension Gen { static func element(of xs: [A]) -> Gen<A?> { return Gen<A?> { rng in xs.randomElement(using: &rng) } } }
LCRNG 17:04
Alright, now we’re dealing with one of our custom generators that Swift’s randomness APIs don’t even tackle: creating a randomly sized array of random values. 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 } } } Missing argument for parameter #1 in call
LCRNG 17:15
The solution here is a bit more delicate, but let’s come back to it later. The remaining compiler errors are much easier to solve first.
LCRNG 17:24
We can update zip to pass the rng along. func zip<A, B>(_ ga: Gen<A>, _ gb: Gen<B>) -> Gen<(A, B)> { return Gen<(A, B)> { rng in (ga.run(&rng), gb.run(&rng)) } } And we can do the same for zip4 .
LCRNG 17:40
We can update always to ignore the provided generator. extension Gen { static func always(_ a: A) -> Gen<A> { return Gen { _ in a } } }
LCRNG 17:49
And when we run our color and pixel and rect and imageView generators, we just need to pass an rng along!
LCRNG 18:07
Just like that, we’ve massively changed what this Gen type is: we’ve baked in the RandomNumberGenerator protocol deep inside the type so that at the moment of creating a Gen value you are provided a RandomNumberGenerator that you can use on the inside to do your work. That means you’re free to map and zip along a bunch of operations to create new generators without ever worrying about the underlying protocol. It’s only at the moment that you call run are you asked to pick a RandomNumberGenerator to use.
LCRNG 18:48
Instead of using the SystemRandomNumberGenerator in the previous examples, we could use a controllable
LCRNG 19:14
Any no matter how many times we run this code, we get the same four image views.
LCRNG 19:28
We’ve controlled the randomness of Gen but only at the moment of invoking run . We are free to create new generators and create new higher-order operations that take Gen s and return Gen s without ever worrying about the RandomNumberGenerator protocol that is powering the whole thing. Flat‑map on Gen
LCRNG 19:59
There’s only one function that we haven’t updated to fit Gen ’s new API. 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 } } } Missing argument for parameter #1 in call
LCRNG 20:12
The problem here is we don’t have an rng to pass to run. And the unavailability of an rng is the best way to know that you may be doing something wrong if you’re trying to extract out randomness but don’t have an rng to provide. If you are operating in a context that doesn’t have an rng and youi’re trying to hit a run you know that you’re doing something wrong.
LCRNG 20:35
One thing we can do is open up and return a new Gen to bring an rng into scope, and we can move the previous work inside, and hopefully pass the rng along to self.run . extension Gen { func array(of count: Gen<Int>) -> Gen<[A]> { return Gen<[A]> { rng in count.map { count in var array: [A] = [] for _ in 1...count { array.append(self.run(&rng)) } return array } } } }
LCRNG 20:51
Huh, we have two problems, so let’s address them one by one. Escaping closures can only capture inout parameters explicitly by value The inner one we’ve encountered before: because map is escaping, we can’t call run with our inout RandomNumberGenerator without explicitly making a local copy. We know this isn’t a good solution, but it’ll at least quiet the error. extension Gen { func array(of count: Gen<Int>) -> Gen<[A]> { return Gen<[A]> { rng in var rng = rng return count.map { count in var array: [A] = [] for _ in 1...count { array.append(self.run(&rng)) } return array } } } } Cannot convert return expression of type ‘Gen<_>’ to return type ‘[A]’
LCRNG 21:18
And now we get a slightly better second error, where it can’t convert a Gen value to an [A] value, which makes sense, because the Gen<[A]> initializer expects a return value of [A] , but in calling map on count , we’re actually returning a Gen<[A]> , which means we’re really producing a Gen<Gen<[A]>> . return Gen<Gen<[A]>> { rng -> Gen<[A]> in var rng = rng return count.map { count in var array: [A] = [] for _ in 1...count { array.append(self.run(&rng)) } return array } }
LCRNG 21:54
But this isn’t correct because the entire array(of:) function wants to return a Gen<[A]> , not the nested generator we’ve ended up with.
LCRNG 22:05
One thing we could do is call run on the result of map with the rng in scope. extension Gen { func array(of count: Gen<Int>) -> Gen<[A]> { return Gen<[A]> { rng -> [A] in var rng = rng return count.map { count in var array: [A] = [] for _ in 1...count { array.append(self.run(&rng)) } return array } .run(&rng) } } }
LCRNG 22:25
Even though this compiles, it just doesn’t seem to be the right way to implement this function. We don’t want to make a copy of the generator for all the reasons we covered earlier.
LCRNG 22:40
One way to avoid the copy is to invert things so that rng is introduced in the body of map . extension Gen { func array(of count: Gen<Int>) -> Gen<[A]> { return count.map { count in Gen<[A]> { rng in var array: [A] = [] for _ in 1...count { array.append(self.run(&rng)) } return array } .run(&rng) } } } Use of unresolved identifier ‘rng’
LCRNG 22:58
We’re no longer making a copy or getting an error about making a copy, but this still can’t work because we can no longer call run on our inner generator to flatten it.
LCRNG 23:22
I think we need to start from scratch. Let’s comment things out and, rather than rely on map , let’s call run on all of our Gen s manually, including count . extension Gen { func array(of count: Gen<Int>) -> Gen<[A]> { // return count.map { count in // Gen<[A]> { rng in // var array: [A] = [] // for _ in 1...count { // array.append(self.run(&rng)) // } // return array // }.run // } return Gen<[A]> { rng in let numberOfElements = count.run(&rng) var result: [A] = [] (0..<numberOfElements).forEach { idx in array.append(self.run(&rng)) } return result } } }
LCRNG 24:14
And now we have a perfectly valid implementation of this function! It’s a very direct ad hoc way of solving the problem we were having. It’s only made of pieces that work for this particular example, but at least it did the job and avoided the problems we were seeing. All we did was have to run generators manually a couple times: once to get the count and then another time to run self .
LCRNG 24:35
But we can still have the directness of our new solution with the abstraction our earlier solution was trying to capture: where we map on count and try to reuse that unit of work. The crux of the problem was the fact that we were led to a Gen<Gen<[A]>> : it was a nesting problem we’ve seen whenever we map on functions that return generic containers.
LCRNG 25:04
At the beginning of the year we spend five episodes looking at this nesting problem, how it can go wrong, and how to solve it ( part 1 , part 2 , part 3 , part 4 , part 5 ), and it turned out that the flatMap function solves this problem!
LCRNG 25:15
Many generic types that have map and zip also have flatMap , which is exactly what we want here. extension Gen { func flatMap<B>(_ f: @escaping (A) -> Gen<B>) -> Gen<B> { return Gen<B> { rng in let a = self.run(&rng) let genB = f(a) let b = genB.run(&rng) return b } } }
LCRNG 26:30
So flatMap on Gen is just a matter of calling run twice: one time to get a random value for the transform function, and another time to get a random value from the Gen that the transform returns.
LCRNG 26:44
In fact, we can look at our manual definition of array(of:) and see this is exactly the work that we’ve sprinkled about in an ad hoc manner. return Gen<[A]> { rng in let numberOfElements = count.run(&rng) var result: [A] = [] (0..<numberOfElements).forEach { idx in array.append(self.run(&rng)) } return result }
LCRNG 26:54
With flatMap at our disposal, we can recover our original implementation. In fact, we can comment it back in, change map to flatMap , remove an extra run , and it just works! extension Gen { func array(of count: Gen<Int>) -> Gen<[A]> { return count.flatMap { count in Gen<[A]> { rng in var array: [A] = [] for _ in 1...count { array.append(self.run(&rng)) } return array } } } }
LCRNG 27:06
Now we were able to reuse that unit of work that flatMap encompasses, and the work being done inside flatMap is completely domain-specific to building an array of random values: we got to remove that extra unit of work of selecting a random number of elements, because that happens implicitly when we call flatMap .
LCRNG 27:33
We’ve now fixed everything in our playground that has to do with the Gen type after we made that broad refactoring where we baked the RandomNumberGenerator protocol directly into the run function. In doing so we got to see what can go wrong when working with randomness! It became a big red flag when run was being invoked in a context that didn’t have a RandomNumberGenerator . The type now leads us to a better design: it forces us to open up a new context whenever we need an RNG, and seems to have solved a bunch of problems along the way!
LCRNG 28:28
It was also cool to see how we were kind of led to rediscover flatMap yet again when it forced us to consider a nesting problem. Any ergonomics
LCRNG 28:41
Things are looking pretty good, and we could probably ship this library as-is, but I think the ergonomics are still a little heavy-handed. We’ve built this AnyRandomNumberGenerator type to solve a real world problem of protocols not conforming to themselves, but we can maybe avoid exposing this detail in everyday use.
LCRNG 29:03
Let’s look at Gen again. struct Gen<A> { let gen: (inout AnyRandomNumberGenerator) -> A } The interface explicitly requires an AnyRandomNumberGenerator , so whatever generator we pick, we need to do a bit of extra work to wrap it up, but maybe we can hide that extra bit of wrapping as a detail inside Gen by exposing a public run interface that works with a generic generator instead. struct Gen<A> { let run: (inout AnyRandomNumberGenerator) -> A func run<RNG: RandomNumberGenerator>(using rng: inout RNG) -> A { var arng = AnyRandomNumberGenerator(rng: rng) return self.gen(&arng) } }
LCRNG 30:01
Now we should be able to simplify our call site by passing in any old generator directly. We can start by unwrapping our SystemRandomNumberGenerator . var srng = AnyRandomNumberGenerator( rng: SystemRandomNumberGenerator() ) To no longer depend on AnyRandomNumberGenerator : var srng = SystemRandomNumberGenerator()
LCRNG 30:15
And we can update our old call sites: Gen<CGFloat>(in: 0...1).run(&srng) To use the public using interface. Gen<CGFloat>(in: 0...1).run(using: &srng)
LCRNG 30:20
There are a bunch more in this file, so let’s fix them quickly.
LCRNG 30:38
Even better, we can unwrap our controlled, anylcrng and pass lcrng directly to the pixelImageView generator with the new using interface.
LCRNG 30:51
But what happened to the randomness? We’re getting the same value every time! If we take a look at the generator’s seed before and after we call run , it’s completely unchanged.
LCRNG 31:10
We forgot a crucial detail in our run function: we’re not actually updating the inout parameter!
LCRNG 31:25
When we forget to mutate a var , Swift helpfully warns us. For example: var a = 1 _ = a + 1 Swift usually produces a warning here, though this is disabled at the playground top level. If we wrap things in a do block, we’ll see the warning in action. do { var a = 1 _ = a + 1 } Variable ‘a’ was never mutated; consider changing to ‘let’ constant
LCRNG 31:49
And there we have it: a very helpful warning to ensure we’re actually mutating something that we tell the compiler we should be mutating.
LCRNG 31:59
Unfortunately it looks like this doesn’t apply to inout variables. It’d be nice if this changed in the future, but for now we just have to be aware that if we declare something inout and aren’t mutating it, we’ve got a problem!
LCRNG 32:13
The problem with our run function is we’re copying our rng when we pass it to AnyRandomNumberGenerator , and in case of our
LCRNG 32:24
We could have updated
LCRNG 32:38
A better solution is to actually mutate the input! struct Gen<A> { let run: (inout AnyRandomNumberGenerator) -> A func run<G: RandomNumberGenerator>(using rng: inout G) -> A { var arng = AnyRandomNumberGenerator(rng) let result = self.gen(&arng) rng = arng.rng as! G return result } } We can reassign rng after the AnyRandomNumberGenerator is mutated. We have to force-cast it because the property on AnyRandomNumberGenerator is an instance of the RandomNumberGenerator protocol, but this is safe to do given that we’re responsible for creating the AnyRandomNumberGenerator and know that we created it with a G .
LCRNG 33:15
And now, when we generate our random image views, randomness has been restored. pixelImageView.run(using: &lcrng) pixelImageView.run(using: &lcrng) pixelImageView.run(using: &lcrng) pixelImageView.run(using: &lcrng) // All four images are different. And randomness is consistent with
LCRNG 33:33
Alright, we were able to get rid of that AnyRandomNumberGenerator detail and we’re left with an API that is feeling as nice as what we started with, but controllable in the same manner as Swift’s APIs. So maybe it’s a good time to ask: what’s the point? Why did we control Gen ? Why did we take the path we took?
LCRNG 34:01
When we first introduced Gen , the point was that Swift gives us randomness APIs that aren’t composable, so we introduced a type that made randomness composable. But now we wanted to see what it took to make randomness testable. We started by showing that Swift’s APIs have testability baked in, and we even introduced a means of controlling those APIs in a global way, which made Swift’s APIs seem really nice.
LCRNG 34:35
Then we switched gears and asked how to introduce testability to our alternate Gen type, but we hit a couple of road blocks that hurt the Gen API: first, by introducing a library-level global, and then by inserting using everywhere up front, forcing generators to either pick their generator when they’re first made, or requiring a ton of extra wrapper code to thread generators through more generically.
LCRNG 35:00
When we took a step back, what we found was a really wonderful idea: by taking the dependency up front as an input argument of the wrapped function, we were able to recover testability without sacrificing composability. We can map , zip , and flatMap to new generators without worrying about what random number generator is powering it under the hood. Only at the moment you call run and pass in whatever random number generator you need.
LCRNG 35:40
Function composition and functional programming and this idea of providing an input as configuration, as a dependency up front, solved all of our problems, and gave us a wonderful type that people really should be using!
LCRNG 36:05
And there’s still more to come! We’re going to finally look into the idea of property testing sometime in the future. Property testing allows us to send a bunch of random information at an API and make sure that a property holds true for any input. It’s an extremely powerful idea that opens up a lot of possibilities.
LCRNG 36:32
And before then, we’ll have some fun using Gen to build some generative art that’s even testable! Till next time! 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 The State Monad: A Tutorial for the Confused? Brandon Simmons • Oct 24, 2009 The Gen type has a more general shape in the functional programming world as the State monad. In this post Brandon Simmons introduces the type and how it works compared to other flat-mappable types. http://brandon.si/code/the-state-monad-a-tutorial-for-the-confused/ Haskell/Understanding monads/State Wikibooks contributors • Feb 27, 2019 A concise description of the state monad from the perspective of Haskell. Uses an example of a random dice roll as motiviation for how state can evolve in a program. https://en.wikibooks.org/wiki/Haskell/Understanding_monads/State Downloads Sample code 0048-predictable-randomness-pt2 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 .