Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

Great series. He really gets to the major problem with OOP. It's not at all obvious what should be modeled or how and changing things once they're wrong is close to impossible. No wonder so few beginners pick it up and do it well. In the last part, what he's describing reminds me of clojure. A language where both data and functions are first class citizens and where functions operating on data is the default paradigm. That's the huge difference I see: in a language oriented around data like clojure, the architecture he describes is almost obvious from the start to experienced programmers and it can be slowly built even if one starts on the wrong path. In OOP languages, there are so many wrong paths and the right path is almost never clear or obvious. Even when the right path is clear, getting there from the current state of the code might be impossible without starting over (think how one would get from an architecture based on his first examples to one based around rules without a rewrite). While the author defends his use of OOP, it's clear imo he's still using the wrong tool for the job. Why use an OOP language that makes data a second class citizen hidden in objects rather than a first class citizen that the language is designed around? As they say, show me your data structures and I will know your architecture. Show me your code/functions and I know nothing.


This is exactly my impression after taking a quick glance through these articles as well.

If anyone is curious and would like to see a practical demonstration of this data-oriented approach of modelling problems, I highly recommend this talk from Mark Bastian in Clojure/conj 2015: https://www.youtube.com/watch?v=Tb823aqgX_0

In it, he contrasts the data-oriented modelling approach with a traditional OOP approach for implementing a board game, and was able to come up with a complete implementation of the game with the former approach that was shorter than even the structural boilerplate for the OOP version.

Another related talk that I loved was Chris Granger's 2013 talk on Light Table: https://www.youtube.com/watch?v=V1Eu9vZaDYw

Where he walks through his process of building a game in ClojureScript using an Entity-Component-System architecture, which is very well suited to this data oriented modeling approach.


Second that for Clojure. OOP is a mental straightjacket. When I started back-end programming I was faced with a fork in the road signposted on one side "Java" and on the other "Perl". I spent a long time with Dietel and Dietel's Java tome but somehow OOP just felt wrong. In Perl you just seemed to get on with it. Data and functions. Yes, you could bless a hash to get your OOP if you really wanted to but it was only later that the community became obsessed with OOP and Moose. So, I travelled down the Perl road and consequently found functional programming, and Clojure in particular, to be natural and easy to learn. Listening to other programmers who followed the traditional OOP path, I get the impression that they will defend OOP even when it's glaring deficiencies are staring them in the face. What is it about OOP? I don't get it. Get rid of it and give yourself a chance to approach problems differently. Bottom-up programming, especially with Clojure and its REPL, make programming a real joy. OOP is Stockholm syndrome masochism.


If you look at modern Java web application code, there’s barely any object orientation to speak of.

(Now it’s mostly Annotation Oriented Programming, especially with Spring. Which brings its own difficulties.)


You know you're in a world of OOP pain, especially with Java, when you have to employ all manner of scaffolding - annotations, dependency injection, presenters, service objects - before you begin the main task of solving your business problem.


Talk is cheap, so are Rich Hickeys dime store musings on OOP.

Show us some code, how would you solve it with functions and data?


In Haskell, this would look something like:

  data Player =
      Player (Maybe Weapon) Class
  
  data Weapon =
        Sword
      | Staff
      | Dagger
  
  data Class =
        Warrior
      | Wizard
  
  type Error = String
  
  mkPlayer :: Maybe Weapon -> Class -> Either Error Player
  
  mkPlayer (Just Sword) Warrior = Right (Player (Just Sword) Warrior)
  mkPlayer (Just Dagger) Warrior = Right (Player (Just Dagger) Warrior)
  mkPlayer Nothing Warrior = Right (Player Nothing Warrior)
  mkPlayer (Just Staff) Warrior = Left "A Warrior cannot equip a Staff"
  
  mkPlayer (Just Staff) Wizard = Right (Player (Just Staff) Wizard)
  mkPlayer (Just Dagger) Wizard = Right (Player (Just Dagger) Wizard)
  mkPlayer Nothing Wizard = Right (Player Nothing Wizard)
  mkPlayer (Just Sword) Wizard = Left "A Wizard cannot equip a Sword"
A player is always a defined class(wizard or warrior), but they may not have a weapon equipped. This solution is a bit wordy, but comes with the benefit that if you ever add a new weapon/class, the compiler will scream at you if you haven't handled the case for it properly.

You would only export the mkPlayer function in the library and you could potentially have much fancier error handling, such as building a data structure that contains an 'invalid' player anyways (e.g. `Left (Player (Just Sword) Wizard)`) so you can custom build an error message at the call site ("A $class cannot equip a $weapon") or even completely ignore the error if that is a potential usecase (such as building an armor/weapon preview tool, where you don't care whether they can use the weapon/armor).

Modifying it is pretty easy too. Say I wanted to allow for 2handed weapons, plus offhand weapons (shields, orbs, charms, etc.) I could encode that in a data type like:

  data EquippedWeapon =
        TwoHanded TwoHandWeapon
      | OneHanded (Maybe OneHandWeapon) (Maybe Offhand)
      | Unequipped
and swap it into the Player definition:

  data Player =
      Player EquippedWeapon Class
And now I wouldn't be able to compile until I fixed the mkPlayer function and any other place that uses a Player and is dependent upon the weapon portion of the data structure.

e.g. This function wouldn't need to change

  areYouAWizardHarry :: Player -> Bool
  areYouAWizardHarry (Player _ Wizard) = True
  areYouAWizardHarry (Player _ _) = False


> This solution is a bit wordy

I'm not a Haskell programmer, but I understood it. It looks like an ML language but with a lack of | and * for guards and tuples. I like your solution a lot.

The main features which allows you to code this solution in such a safe way are the Maybe and Either types. It's high time OO programmers - and OO programming languages - learn the lessons FP languages have taught us and include these constructs in the standard library. They're just so much cleaner than the usual alternatives (nullable types, checked exceptions) and there's no reason they can't be defined as small objects.


I'm going to be That Guy and suggest you give Rust a try. It's got the best of imperative and functional mixed in.


The implication being that OO is the same thing is imperative? I'm in strong disagreement with that!

I've tried to learn rust a few times, but never with much tenacity. It's on my list because it seems to hit a good point wrt expressiveness and performance.


Not what I meant, no.

In oversimplified terms, Rust has objects but not classes. It skews more toward: - from a C dev's perspective: data-driven design - from a Haskell dev's perspective: typeclasses and ADTs


From what I can tell (I'm no Haskell programmer), in that solution there's no declared relationship between the class and the weapons they accept, right? It's just a side-effect from the fact that you can't create a Player without passing through the gauntlet of mkPlayer?

If so, is that a practicality issue, or an Haskell limitation?


The answer is very much "practicality issue". Haskell's more advanced type level features (including GADTs and type families) are very much suited for this, but they're also the sort of thing that gives Haskell a reputation for being complicated. If your just using Haskell's core features the way the parent post does, Haskell is a very simple, very elegant language.

But better yet, it certainly does have the big guns which you can pull out.

    -- Just like before, we define `Class` and `Weapon`:
    data Class = Warrior | Wizard
    data Weapon = Sword | Staff | Dagger

    -- The one really annoying thing is that
    -- at the moment you have to use a little bit
    -- of annoying boilerplate to define singletons
    -- (not related to the OOP concept of singletons, by
    -- the way), or use the `singletons` library. In the
    -- future, with DependentHaskell, this won't be necessary:
    data SWeapon (w :: Weapon) where
      SSword :: SWeapon 'Sword
      SStaff :: SWeapon 'Staff
      SDagger :: SWeapon 'Dagger

    -- Now we can define `Player`:
    data Player (c :: Class) where
      WizardPlayer :: AllowedToWield 'Wizard w ~ 'True => SWeapon w -> Player 'Wizard
      WarriorPlayer :: AllowedToWield 'Warrior w ~ 'True => SWeapon w -> Player 'Warrior
This last part shouldn't be to difficult to understand, if you ignore the SWeapon boilerplate: Player is parameterized over the player's class, with different constructors for warriors and wizards. Each constructor has a parameter for the weapon the player is wielding, which is constrained by the type family (read: type-level function) named AllowedToWield.

AllowedToWield isn't that complicated either, it's just a (type-level) function that takes a Class and a Weapon and returns a `Bool` using pattern matching:

    type family AllowedToWield (c :: Class) (w :: Weapon) :: Bool where
      AllowedToWield 'Wizard 'Sword = 'False
      AllowedToWield 'Wizard 'Dagger = 'True
      AllowedToWield 'Wizard 'Staff = 'True
      AllowedToWield 'Warrior 'Sword = 'True
      AllowedToWield 'Wizard 'Dagger = 'True
      AllowedToWield 'Wizard 'Staff = 'False
And there it is. What do you gain from all this? Something which it is very had to get in certain other languages: compile-time type checking that there is no code that will allow a wizard to equip a sword, or a warrior to equip a staff.

Once again, I want to make it clear that you absolutely don't need to do this, even in Haskell. You're absolutely allowed to write the simple code like in the parent post. But in my opinion, this is an extremely powerful and useful tool that Haskell lets you take much further than many other languages.

So long story short, the answer to your question is that it is indeed a "practicality issue", although I don't think that my code is that impracticable. It certainly is absolutely not a Haskell limitation: in fact if anything, Haskell makes it a bit too tempting to go in the other direction, and go way overboard with embedding this kind of thing in the type system.


Thanks for the detailed explanation! I'm mostly a dynamic languages programmer, but I've been reading (and enjoying) Type-Driven Development with Idris, and I have a plan to learn Haskell after that. If such a relationship wasn't modellable, I'm not sure I would have bothered after all.


mkPlayer can be simplified further!

  mkPlayer :: Maybe Weapon -> Class -> Either Error Player

  mkPlayer (Just Staff) Warrior = Left "A Warrior cannot equip a Staff"
  mkPlayer (Just Sword) Wizard = Left "A Wizard cannot equip a Sword"
  mkPlayer weapon klass = Right (Player weapon klass)


You lose exhaustiveness checks if new weapons or classes were added though. May or may not be worth the trade-off.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: