-
-
Save minsOne/968611a85fffc38e97b22df28017d7c8 to your computer and use it in GitHub Desktop.
Typestate in Swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
enum Parked {} | |
enum Driving {} | |
enum Gaming {} | |
private class EngineSystem { | |
static var shared = EngineSystem() | |
private init() {} | |
func start() {/**/} | |
func accelerate() { /* Uses gas pedal input to accelerate the real car */ } | |
func brake() { /* Uses brake pedal input to deccelerate the real car */ } | |
} | |
private class InfotainmentSystem { | |
static var shared = InfotainmentSystem() | |
private init() {} | |
func launchRacingGame() { /**/ } | |
func accelerate() { /* Uses gas pedal to accelerate in Beach Buggy Racing game */ } | |
func brake() { /* Uses brake pedal to deccelerate in Beach Buggy Racing game */ } | |
} | |
struct Tesla<State>: ~Copyable { | |
private let engineSystem: EngineSystem | |
private let infotainmentSystem: InfotainmentSystem | |
private init(engineSystem: EngineSystem, infotainmentSystem: InfotainmentSystem) { | |
self.engineSystem = engineSystem | |
self.infotainmentSystem = infotainmentSystem | |
} | |
} | |
extension Tesla where State == Parked { | |
init() { | |
self.init( | |
engineSystem: EngineSystem.shared, | |
infotainmentSystem: InfotainmentSystem.shared | |
) | |
} | |
consuming func launchRacingGame() -> Tesla<Gaming> { | |
Tesla<Gaming>( | |
engineSystem: engineSystem, | |
infotainmentSystem: infotainmentSystem | |
) | |
} | |
consuming func drive() -> Tesla<Driving> { | |
Tesla<Driving>( | |
engineSystem: engineSystem, | |
infotainmentSystem: infotainmentSystem | |
) | |
} | |
} | |
extension Tesla where State == Driving { | |
consuming func park() -> Tesla<Parked> { | |
Tesla<Parked>( | |
engineSystem: engineSystem, | |
infotainmentSystem: infotainmentSystem | |
) | |
} | |
func accelerate() { | |
engineSystem.accelerate() | |
} | |
func brake() { | |
engineSystem.brake() | |
} | |
} | |
extension Tesla where State == Gaming { | |
consuming func exitRacingGame() -> Tesla<Parked> { | |
Tesla<Parked>( | |
engineSystem: engineSystem, | |
infotainmentSystem: infotainmentSystem | |
) | |
} | |
func accelerate() { | |
infotainmentSystem.accelerate() | |
} | |
func brake() { | |
infotainmentSystem.brake() | |
} | |
} | |
func main() { | |
var parked = Tesla<Parked>() | |
let gaming = parked.launchRacingGame() | |
gaming.accelerate() // accelerating in Beach Buggy Racing game | |
gaming.brake() // braking in Beach Buggy Racing game | |
parked = gaming.exitRacingGame() | |
let driving = parked.drive() | |
driving.accelerate() // accelerating the real car | |
driving.launchRacingGame() // ❌ ERROR: can't game while driving | |
} | |
enum TeslaState: ~Copyable { | |
case driving(Tesla<Driving>) | |
case parked(Tesla<Parked>) | |
case gaming(Tesla<Gaming>) | |
init() { self = .parked(Tesla<Parked>()) } | |
mutating func launchRacingGame() { | |
switch consume self { | |
case let .driving(driving): | |
self = .driving(driving) | |
case let .parked(parked): | |
self = .gaming(parked.launchRacingGame()) | |
case let .gaming(gaming): | |
self = .gaming(gaming) | |
} | |
} | |
mutating func accelerate() { | |
switch consume self { | |
case let .driving(driving): | |
driving.accelerate() | |
self = .driving(driving) | |
case let .parked(parked): | |
self = .parked(parked) | |
case let .gaming(gaming): | |
gaming.accelerate() | |
self = .gaming(gaming) | |
} | |
} | |
mutating func brake() {/**/} | |
mutating func drive() {/**/} | |
mutating func park() {/**/} | |
} | |
func main2() { | |
var tesla = TeslaState() | |
tesla.drive() // Tesla<Driving> | |
tesla.accelerate() // accelerating a real car | |
tesla.launchRacingGame() // does nothing | |
tesla.park() // Tesla<Parked> | |
tesla.launchRacingGame() // Tesla<Gaming> | |
tesla.accelerate() // accelerating in Beach Buggy Racing | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
enum Locked {} | |
enum Unlocked {} | |
struct Turnstile<State>: ~Copyable { | |
private(set) var coins: Int | |
private init(coins: Int) { | |
self.coins = coins | |
} | |
} | |
extension Turnstile where State == Locked { | |
init() { | |
self.init(coins: 0) | |
} | |
consuming func insertCoin() -> Turnstile<Unlocked> { | |
Turnstile<Unlocked>(coins: coins + 1) | |
} | |
} | |
extension Turnstile where State == Unlocked { | |
consuming func push() -> Turnstile<Locked> { | |
Turnstile<Locked>(coins: coins) | |
} | |
} | |
func illegalOperationsAreImpossible() { | |
var locked = Turnstile<Locked>() | |
var unlocked = locked.insertCoin() | |
// Uncomment to see errors | |
// unlocked.insertCoin() // ❌ ERROR: Can't insert coin when Unlocked | |
// locked.push() // ❌ ERROR: Can't push when Unlocked | |
} | |
func cannotReuseState() { | |
var locked = Turnstile<Locked>() | |
var unlocked = locked.insertCoin() | |
// unlocked = locked.insertCoin() // ❌ ERROR: Can't insert coin again | |
} | |
func strictOrderOfOperations() { | |
var locked = Turnstile<Locked>() | |
.insertCoin() | |
.push() | |
.insertCoin() | |
.push() | |
print(locked.coins) // 2 | |
locked | |
.insertCoin() | |
// .insertCoin() // ❌ ERROR: can't insertCoin again | |
.push() | |
// .push() // ❌ ERROR: can't push again | |
} | |
enum TurnstileState: ~Copyable { | |
case locked(Turnstile<Locked>) | |
case unlocked(Turnstile<Unlocked>) | |
init() { | |
self = .locked(Turnstile<Locked>()) | |
} | |
mutating func coins() -> Int { | |
let coins: Int | |
switch consume self { | |
case let .locked(locked): | |
coins = locked.coins | |
self = .locked(locked) | |
case let .unlocked(unlocked): | |
coins = unlocked.coins | |
self = .unlocked(unlocked) | |
} | |
return coins | |
} | |
mutating func insertCoin() { | |
switch consume self { | |
case let .locked(locked): | |
let unlocked = locked.insertCoin() | |
self = .unlocked(unlocked) | |
case let .unlocked(unlocked): | |
self = .unlocked(unlocked) | |
} | |
} | |
mutating func push() { | |
switch consume self { | |
case let .locked(locked): | |
self = .locked(locked) | |
case let .unlocked(unlocked): | |
let locked = unlocked.push() | |
self = .locked(locked) | |
} | |
} | |
} | |
func combinedInterface() { | |
var turnstile = TurnstileState() | |
turnstile.insertCoin() // unlocked | |
turnstile.insertCoin() // does nothing | |
turnstile.push() // locked | |
turnstile.push() // does nothing | |
print(turnstile.coins()) // 1 | |
} | |
// Example of a non-deterministic transition | |
extension Turnstile where State == Locked { | |
enum TransitionResult: ~Copyable { | |
case locked(Turnstile<Locked>) | |
case unlocked(Turnstile<Unlocked>) | |
} | |
consuming func tryInsertCoin(coinDiameter: Decimal) -> TransitionResult { | |
if coinDiameter <= 5 { | |
return .unlocked(Turnstile<Unlocked>(coins: coins + 1)) | |
} else { | |
return .locked(self) | |
} | |
} | |
} | |
extension TurnstileState { | |
mutating func tryInsertCoin(coinDiameter: Decimal) { | |
switch consume self { | |
case let .locked(locked): | |
let result = locked.tryInsertCoin(coinDiameter: coinDiameter) | |
switch consume result { | |
case let .locked(locked): | |
self = .locked(locked) | |
case let .unlocked(unlocked): | |
self = .unlocked(unlocked) | |
} | |
case let .unlocked(unlocked): | |
self = .unlocked(unlocked) | |
} | |
} | |
} | |
func nonDeterministicTransition() { | |
var turnstile = TurnstileState() | |
turnstile.tryInsertCoin(coinDiameter: 10) // still locked | |
turnstile.tryInsertCoin(coinDiameter: 2) // unlocked | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment