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.
Subscribe to Point-Free
Access this episode, plus all past and future episodes when you become a subscriber.
Already a subscriber? Log in
Exercises
We’ve all but completely recovered the ergonomics of
Gen
from before we controlled it, but our publicrun
function requires an explicitRandomNumberGenerator
is passed in as a dependency. Add an overload to recover the ergonomics of callinggen.run()
without aRandomNumberGenerator
.Solution
The problem here is a bare
run
function collides with therun
property. This isn’t a big blocker, though, because the property is a private concern. We can rename it to anything, like_run
orgen
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) } }
The
Gen
type perfectly encapsulates producing a random value from a given mutable random number generator. GeneralizeGen
to a typeState
that produces values from a given mutable parameter.Solution
Generalizing
Gen
toState
requires us to introduce another generic to take the place ofAnyRandomNumberGenerator
.struct State<S, A> { let run: (inout S) -> A }
Recover
Gen
as a specification ofState
using a type alias.Solution
Generic type aliases allow us to fix any generic, so we can set
S
toAnyRandomNumberGenerator
.typealias Gen<A> = State<AnyRandomNumberGenerator, A>
Deriving
Gen
as a type alias ofState
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 extendingGen
and working with theGen
type internally, which fixes theS
parameter ofState
toAnyRandomNumberGenerator
. Let’s redefine things without any mention ofGen
.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. Unfortunatelyextension Gen
is not the same asextension State where S == AnyRandomNumberGenerator
, so we need to manually constrain things to have things behave nicely. It helps to define these extensions without mentioningGen
.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, 2017This 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, 2009The 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, 2019A 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.