Predictable Randomness: Part 2

Episode #48 • Feb 25, 2019 • Subscriber-Only

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.

Predictable Randomness: Part 2
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

Unlock This Episode

Our Free plan includes 1 subscriber-only episode of your choice, plus weekly updates from our newsletter.

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.

This episode is for subscribers only.

Subscribe to Point-Free

Access this episode, plus all past and future episodes when you become a subscriber.

See plans and pricing

Already a subscriber? Log in

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

SE-0202: Random Unification

Alejandro Alonso • Friday 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.

The State Monad: A Tutorial for the Confused?

Brandon Simmons • Saturday 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.

Haskell/Understanding monads/State

Wikibooks contributors • Wednesday 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.