|
|
|
|
|
by SamReidHughes
4467 days ago
|
|
> don't think we're on the same page here. I meant to say that Haskell protects against inconsistent states: you literally cannot create a dead player with HP and Inventory records. I don't have a strong grasp on the visitor pattern, but I'm not sure how it can be used to accomplish compile time guarantees that the properties of an object will always be correct. An example of the visitor pattern: class LivePlayer;
class DeadPlayer;
class PlayerVisitor {
public:
virtual void VisitLive(LivePlayer *p) = 0;
virtual void VisitDead(DeadPlayer *p) = 0;
};
class Player {
public:
virtual void Visit(PlayerVisitor *v) = 0;
virtual ~Player() { }
};
class LivePlayer : public Player {
public:
void Visit(PlayerVisitor *v) { v->VisitLive(this); }
LivePlayer(int hp, std::vector<InventoryItem> inventory)
: hp_(hp), inventory_(inventory) { }
int hp() const { return hp_; }
const std::vector<InventoryItem> &inventory() const { return inventory_; }
private:
int hp_;
std::vector<InventoryItem> inventory_;
};
class DeadPlayer : public Player {
public:
void Visit(PlayerVisitor *v) { v->VisitDead(this); }
DeadPlayer() { }
};
This is equivalent to the Haskell code data Player = LivePlayer Int (Vector InventoryItem) | DeadPlayer
including the fact that in this example the objects are immutable.Instead of using a case expression to pattern match over the player, you'd have to construct a PlayerVisitor and implement the visit methods on that type. Things can be made a bit less cumbersome than that, for example in a language with lambdas, you can just have the Visit method take a lambda for each subclass, and each subclass's implementation calls one. That makes it arguably equally convenient to use the types (with some extra parentheses), but it's still much more annoying to define the types that way in the first place. |
|