Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Type Erasure: Have I missed Anything?

Tags:

generics

swift

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() }
    }
}
like image 607
Verticon Avatar asked Dec 05 '16 17:12

Verticon


1 Answers

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.

like image 63
Rob Napier Avatar answered Sep 28 '22 11:09

Rob Napier