A video series exploring functional programming and Swift.
#48 • Monday Feb 25, 2019 • Subscriber-only

Predictable Randomness: Part 2

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.

This episode builds on concepts introduced previously:

#48 • Monday Feb 25, 2019 • Subscriber-only

Predictable Randomness: Part 2

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.

This episode builds on concepts introduced previously:


Subscribe to Point‑Free

This episode is for subscribers only. To access it, and all past and future episodes, become a subscriber today!

See subscription optionsorLog in

Sign up for our weekly newsletter to be notified of new episodes, and unlock access to any subscriber-only episode of your choosing!

Sign up for free episode

Recap

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.

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.

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.

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.

Subscribe to Point-Free

👋 Hey there! Does this episode sound interesting? Well, then you may want to subscribe so that you get access to this episodes and more!


Exercises

  1. We’ve all but completely recovered the ergonomics of Gen from before we controlled it, but our public run function requires an explicit RandomNumberGenerator is passed in as a dependency. Add an overload to recover the ergonomics of calling gen.run() without a RandomNumberGenerator.

    Solution

    The problem here is a bare run function collides with the run property. This isn’t a big blocker, though, because the property is a private concern. We can rename it to anything, like _run or gen and recover a really nice public interface.

    struct Gen<A> {
      let gen: (inout AnyRandomNumberGenerator) -> A
    
      func run<RNG: RandomNumberGenerator>(using rng: inout RNG) -> A {
        var arng = AnyRandomNumberGenerator(rng: rng)
        let result = self.run(&rng)
        rng = arng.rng as! RNG
        return result
      }
    
      func run() -> A {
        var srng = SystemRandomNumberGenerator()
        return self.run(using: &srng)
      }
    }
    
  2. The Gen type perfectly encapsulates producing a random value from a given mutable random number generator. Generalize Gen to a type State that produces values from a given mutable parameter.

    Solution

    Generalizing Gen to State requires us to introduce another generic to take the place of AnyRandomNumberGenerator.

    struct State<S, A> {
      let run: (inout S) -> A
    }
    
  3. Recover Gen as a specification of State using a type alias.

    Solution

    Generic type aliases allow us to fix any generic, so we can set S to AnyRandomNumberGenerator.

    typealias Gen<A> = State<AnyRandomNumberGenerator, A>
    
  4. Deriving Gen as a type alias of State breaks a bunch of implementations, including:

    • map
    • flatMap
    • int(in:)
    • float(in:)
    • bool

    Update each implementation for State.

    Solution

    We’re currently defining map by extending Gen and working with the Gen type internally, which fixes the S parameter of State to AnyRandomNumberGenerator. Let’s redefine things without any mention of Gen.

    extension State {
      func map<B>(_ f: @escaping (A) -> B) -> State<S, B> {
        return State<S, B> { s in f(self.run(&s)) }
      }
    }
    

    And flatMap can be updated similarly.

    extension State {
      func flatMap<B>(_ f: @escaping (A) -> State<S, B>) -> State<S, B> {
        return State<S, B> { s in
          f(self.run(&s)).run(&s)
        }
      }
    }
    

    Our static helpers on Gen need to give the type system a bit more information to behave. Unfortunately extension Gen is not the same as extension State where S == AnyRandomNumberGenerator, so we need to manually constrain things to have things behave nicely. It helps to define these extensions without mentioning Gen.

    extension State where S == AnyRandomNumberGenerator, A: FixedWidthInteger {
      static func int(in range: ClosedRange<A>) -> State {
        return State { rng in .random(in: range, using: &rng) }
      }
    }
    
    extension State where S == AnyRandomNumberGenerator, A: BinaryFloatingPoint, A.RawSignificand: FixedWidthInteger {
      static func int(in range: ClosedRange<A>) -> State {
        return State { rng in .random(in: range, using: &rng) }
      }
    }
    
    extension State where S == AnyRandomNumberGenerator, A == Bool {
      static let bool = State { rng in .random(using: &rng) }
      }
    }
    

References

Chapters
Recap
00:05
Controlling the Gen type
01:56
Controlling with a global
02:23
Controlling with an argument
04:29
Controlling in the type
10:55
Flat-map on Gen
19:59
Any ergonomics
28:41
What’s the point?
33:33