Hacker News new | ask | show | jobs
by SamReidHughes 4467 days ago
> In C or OOP languages, you just get a player object/struct and you have to enforce this state logic manually, and you can easily write a function that puts the player in an inconsistent state (player.alive = False, but player.HP = 15).

False. In OOP you can do this using the visitor pattern. The difference is that Haskell makes this real convenient, with its algebraic types feature baked into the language, while OOP makes it very cumbersome.

> As a side note on the last point: people make the argument that this separation makes Haskell impossible to do printf debugging. Not true—the standard library provides ways of circumventing the type system specifically for this reason.

It's still far more annoying when the language is lazily evaluated and has Haskell syntax.

2 comments

> False. In OOP you can do this using the visitor pattern. The difference is that Haskell makes this real convenient, with its algebraic types feature baked into the language, while OOP makes it very cumbersome.

I 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.

> It's still far more annoying when the language is lazily evaluated and has Haskell syntax.

Python:

  def f(x,y):
      print x, y
      return x + y
Haskell:

  f x y = traceShow (x, y) (x + y)
Laziness is a separate issue altogether. Debugging lazy behavior is difficult in any language, and you would have similar problems in Python if you were testing deeply nested generators.

That said, laziness should not be an issue if you are simply testing that functions are correct, since most functions are pure and you will almost never use lazy IO.

If you need to debug evaluation order or a memory leak, that's much harder, and admitted one of Haskell's biggest weaknesses. My answer here is that you should almost always prefer strict data structures and functions unless you really need laziness. That's not a cure all, but it is a good policy when programming in Haskell.

> 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.

I think even the visitor pattern isn't required here, all you need is subclassing. Although this clutters things up more by having abstract methods in the base class, technically that's all you need.
Sure.
To be fair, if the place you're trying to debug is not evaluated (be it due to laziness or because it's generally dead code) that should be a pretty strong indicator something is wrong.
And generally a good clue as to what is wrong (or at least where to put the next couple checks...).