Friday, January 11, 2008

Nested records headaches in Haskell

I'm fighting a bit with Haskell records and named fields. This post has some valid remarks, but no real solutions. The main problem I have is managing elegantly nested records:
In my (embryonic) Haskell RPG game, a character is defined as a data type with several named fields (name, gender, traits). Traits is a collection of all characteristics (Strength, Dexterity, etc.). Each trait is itself a record with three values (normal level, current level, experience points). When an action is performed, not only we look at a rating value, we also update the rating experience as a result. So I want a function that takes a character, the name of a characteristic, and can update the proper characteristic rating.
With records
Traits { strength::Rating...}
I get a lookup function: a function strength that I can use to get the current value, so I can have a function like
testTrait :: Character -> (Traits -> Rating) -> ...
Where ... is whatever result the function returns (something like IO(Character,TestResult). But if testTrait needs to modify the record, I don't have a dynamic function to do it! And even if my code could hard code that "strength" is the trait I need, then the update mechanism is singularly cumbersome, since:
let c1=c{traits{strength{experience=1}}}
Doesn't compile (c is a character, I want to set experience of strength to 1). And if I want to modify the value (like do a +1 instead of setting to 1), then I need to unstack everything and rebuild it. Argh! Surely there is a better way?

The solution I have for the moment is that Traits contains only a list of Rating objects. Then I've defined an Enum with a constructor for each characteristic:
data Characteristic = Strength | ...
deriving (Show,Read,Eq,Enum,Bounded)
and I can have a method that combines fromEnum and !! to retrieve the right value. I can also have a little function that let me update a precise element in a list. But of course the type system does not guarantee me that every Character will have the right amount of Rating objects in the array!

It may be that my brain has been totally formatted by years of Java (in Java I could just pass the Rating object and it would get modified in place of course), but I just don't know what structure would give me total safety (enforcing that every character has the proper data) with no boilerplate code and no duplication. (I know I can define helper methods to do all of that, but I though Haskell would let me cut the amount of purely technical code needed and concentrate on the business).

4 comments:

Justin said...

Record syntax is pretty ugly in Haskell, and it's unfortunate. The trick is you need to capture each inner record when you define testTrait, and then reuse those values to build your "modified" value.

I don't know if I divined your data types correctly but you'll see it does reuse the character defined.

testTrait takes a test, an acessor and an update function. I think the update function is missing from your definition and probably what is giving you trouble. As the caller, you have to tell testTrait how to test, what to test, and what to update. I don't know that you could "reflect" the attribute to update based on the test from within testTrait.

When you run the program, the character "Bob" has their strength XP set to 1 in the first case, and their dexterity is unmodified in the second case. Note both cases start with the initial character.

Probably blogger will mangle the code badly - feel free to email me.

---


data Character = Character { charName :: String
, traits :: Traits }
deriving Show

data Traits = Traits { strength, dex :: Rating }
deriving Show

data Rating = Rating { curr, xp :: Int }
deriving Show

testTrait :: Character -> (Traits -> Bool) -> (Traits -> Rating) -> (Traits -> Rating -> Traits) -> (Character, Bool)
testTrait character@(Character { traits = ts }) test access update =
if test ts
then (character { traits = update ts ((access ts) { xp = 1 }) }, True)
else (character, False)

char1 = Character "Bob" (Traits (Rating 2 0) (Rating 1 0))

main = do
let (char1Str, _) = testTrait char1 (const True) strength (\ts r -> ts { strength = r })
(char1Dex, _) = testTrait char1 (const False) dex (\ts r -> ts { dex = r })
putStrLn $ "Example character is " ++ show char1
putStrLn $ "Testing strength gives " ++ show char1Str
putStrLn $ "Testing dex gives " ++ show char1Dex

JP Moresmau said...

Thanks for this. I understand your code (and you've got my data types correctly) but what I don't like is the duplication in testTrait: I need to pass strength and a function that tells the code how to update strength. So it's easy to write buggy code that will read one trait and update another.

Justin said...

Good point. If you parameterize the different ratings by type, and modify the signature for testTrait, then you can ensure the same trait is both accessed and modified. In "Rating p", p is a "phantom type" because it does not show up on the right-hand side. Traits then declares that "strength" and "dex" have different types, even though they are both Ratings. This ensures that testTrait cannot get two different traits, as the various "Rating a" types must all be the same.

If you change the main function to access strength but update dex, you'll get a compile time error, which is exactly what you want to happen. Just the modifications needed are below:

data Rating a = Rating { curr, xp :: Int }

data Strength = Strength
data Dex = Dex

data Traits = Traits { strength :: Rating Strength
, dex :: Rating Dex}

testTrait :: Character -> (Traits -> Bool) -> (Traits -> Rating a) -> (Traits -> Rating a -> Traits) -> (Character, Bool)

JP Moresmau said...

That's sweeter, yes. Your post led me towards "scrap your boilerplate" and Data.Generics, and using generics does allow nice concise syntax I believe. I'll post when I have the code working!