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.
(Now it’s mostly Annotation Oriented Programming, especially with Spring. Which brings its own difficulties.)
Show us some code, how would you solve it with functions and data?
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 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)If so, is that a practicality issue, or an Haskell limitation?
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.
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'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.
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