Let's say I have the following data model, for keeping track of the stats of baseball players, teams, and coaches:
data BBTeam = BBTeam { teamname :: String,
manager :: Coach,
players :: [BBPlayer] }
deriving (Show)
data Coach = Coach { coachname :: String,
favcussword :: String,
diet :: Diet }
deriving (Show)
data Diet = Diet { dietname :: String,
steaks :: Integer,
eggs :: Integer }
deriving (Show)
data BBPlayer = BBPlayer { playername :: String,
hits :: Integer,
era :: Double }
deriving (Show)
Now let's say that managers, who are usually steak fanatics, want to eat even more steak -- so we need to be able to increase the steak content of a manager's diet. Here are two possible implementations for this function:
1) This uses lots of pattern matching and I have to get all of the argument ordering for all of the constructors right ... twice. It seems like it wouldn't scale very well or be very maintainable/readable.
addManagerSteak :: BBTeam -> BBTeam
addManagerSteak (BBTeam tname (Coach cname cuss (Diet dname oldsteaks oldeggs)) players) = BBTeam tname newcoach players
where
newcoach = Coach cname cuss (Diet dname (oldsteaks + 1) oldeggs)
2) This uses all of the accessors provided by Haskell's record syntax, but it is also ugly and repetitive, and hard to maintain and read, I think.
addManStk :: BBTeam -> BBTeam
addManStk team = newteam
where
newteam = BBTeam (teamname team) newmanager (players team)
newmanager = Coach (coachname oldcoach) (favcussword oldcoach) newdiet
oldcoach = manager team
newdiet = Diet (dietname olddiet) (oldsteaks + 1) (eggs olddiet)
olddiet = diet oldcoach
oldsteaks = steaks olddiet
My question is, is one of these better than the other, or more preferred within the Haskell community? Is there a better way to do this (to modify a value deep inside a data structure while keeping the context)? I'm not worried about efficiency, just code elegance/generality/maintainability.
I noticed there is something for this problem (or a similar problem?) in Clojure: update-in
-- so I think that I'm trying to understand update-in
in the context of functional programming and Haskell and static typing.
Here's how you might use semantic editor combinators (SECs), as Lambdageek suggested.
First a couple of helpful abbreviations:
The
Unop
here is an "semantic editor", and theLifter
is the semantic editor combinator. Some lifters:Now simply compose the SECs to say what you want, namely add 1 to the stakes of the diet of the manager (of a team):
Comparing with the SYB approach, the SEC version requires extra work to define the SECs, and I've only provided the ones needed in this example. The SEC allows targeted application, which would be helpful if the players had diets but we didn't want to tweak them. Perhaps there's a pretty SYB way to handle that distinction as well.
Edit: Here's an alternative style for the basic SECs:
Later you may also want to take a look at some generic programming libraries: when the complexity of your data increases and you find yourself writing more and boilerplate code (like increasing steak content for players', coaches' diets and beer content of watchers) which is still boilerplate even in less verbose form. SYB is probably the most well known library (and comes with Haskell Platform). In fact the original paper on SYB uses very similar problem to demonstrate the approach:
(the rest is in the paper - reading is recommended)
Of course in your example you just need to access/modify one piece of a tiny data structure so it does not require generic approach (still the SYB-based solution for your task is below) but once you see repeating code/pattern of accessing/modification you my want to check this or other generic programming libraries.
Record update syntax comes standard with the compiler:
Terrible! But there's a better way. There are several packages on Hackage that implement functional references and lenses, which is definitely what you want to do. For example, with the fclabels package, you would put underscores in front of all your record names, then write
Edited in 2017 to add: these days there is broad consensus on the lens package being a particularly good implementation technique. While it is a very big package, there is also very good documentation and introductory material available in various places around the web.