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:
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.
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.
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.
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:
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 :: 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)
Show us some code, how would you solve it with functions and data?