I decided to get a better understanding of type erasure by writing some simple code. I have a generic Soldier protocol. Soldiers have weapons and soldiers can fight. I would like to create army of different types of Soldiers. I thought that type erasure would provide me with a means of boxing the Soldier adopters such that I could treat them as plain Soldiers (instead of Snipers, Infantrymen, etc.) But I found that the intermediate, boxing type (the type eraser) must still be made generic over Soldier's associated type (i.e. Weapon). So, I can have Rifle wielding Soldiers, or Rocket wielding Soldiers but not just plain Soldiers. Is there anything about the use of type erasure that I have missed?
import Foundation
// Soldiers have weapons and soldiers can fight
protocol Weapon {
func fire()
}
protocol Soldier {
associatedtype W: Weapon
var weapon: W { get }
func fight()
}
extension Soldier {
func fight() { weapon.fire() }
}
// Here are some weapons
struct Rifle : Weapon {
func fire() { print("Bullets away!") }
}
struct Rocket : Weapon {
func fire() { print("Rockets away!") }
}
struct GrenadeLauncher : Weapon {
func fire() { print("Grernades away!") }
}
// Here are some soldiers
struct Sniper : Soldier {
var weapon = Rifle()
}
struct Infantryman : Soldier {
var weapon = Rifle()
}
struct Artillaryman : Soldier {
var weapon = Rocket()
}
struct Grenadier : Soldier {
var weapon = GrenadeLauncher()
}
// Now I would like to have an army of soldiers but the compiler will not let me.
// error: protocol 'Soldier' can only be used as a generic constraint because it has Self or associated type requirements
class Army {
var soldiers = [Soldier]()
func join(soldier: Soldier) {
soldiers.append(soldier)
}
func makeWar() {
for soldier in soldiers { soldier.fight() }
}
}
// So, let's try the type erasure technique:
struct AnySoldier<W: Weapon> : Soldier {
var weapon: W
private let _fight: () -> Void
init<S: Soldier>(soldier: S) where S.W == W {
_fight = soldier.fight
weapon = soldier.weapon
}
func fight() { _fight() }
}
var s1 = AnySoldier(soldier: Sniper())
print (type(of: s1)) // AnySoldier<Rifle>
s1.fight() // Bullets away!
s1.weapon.fire() // Bullets away!
s1 = AnySoldier(soldier: Infantryman()) // Still good; Infantrymen use rifles
s1 = AnySoldier(soldier: Grenadier()) // Kaboom! Grenadiers do not use rifles
// So now I can have an army of Rifle wielding Soldiers
class Army {
var soldiers = [AnySoldier<Rifle>]()
func join(soldier: AnySoldier<Rifle>) {
soldiers.append(soldier)
}
func makeWar() {
for soldier in soldiers { soldier.fight() }
}
}
let army = Army()
army.join(soldier: AnySoldier(soldier: Sniper()))
army.join(soldier: AnySoldier(soldier: Infantryman()))
army.join(soldier: AnySoldier(soldier: Grenadier())) // Kaboom! Rifles only
army.makeWar()
// But, what I really want is an army wherein the weapons are unrestricted.
class Army {
var soldiers = [AnySoldier]()
func join(soldier: AnySoldier) {
soldiers.append(soldier)
}
func makeWar() {
for soldier in soldiers { soldier.fight() }
}
}
You need to type-erase Weapons as well:
struct AnyWeapon: Weapon {
private let _fire: () -> Void
func fire() { _fire() }
init<W: Weapon>(_ weapon: W) {
_fire = weapon.fire
}
}
With this, AnySoldier
does not need to be generic.
struct AnySoldier : Soldier {
private let _fight: () -> Void
let weapon: AnyWeapon
init<S: Soldier>(_ soldier: S) {
_fight = soldier.fight
weapon = AnyWeapon(soldier.weapon)
}
func fight() { _fight() }
}
Don't overlook another approach, though, which is to replace Weapon with a simple function and make Soldier a simple struct. For example:
struct Soldier {
private let weaponFire: () -> Void
func fight() { weaponFire() }
static let sniper = Soldier(weaponFire: { print("Bullets away!") })
}
let sniper = Soldier.sniper
sniper.fight()
I discuss some of this further in Beyond Crusty: Real-World Protocols. Sometimes you don't want a protocol.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With