Is there a Haskell idiom for updating a nested data structure?
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.
Solution 1:
Record update syntax comes standard with the compiler:
addManStk team = team {
manager = (manager team) {
diet = (diet (manager team)) {
steaks = steaks (diet (manager team)) + 1
}
}
}
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
$(mkLabels ['BBTeam, 'Coach, 'Diet, 'BBPlayer])
addManStk = modify (+1) (steaks . diet . manager)
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.
Solution 2:
Here's how you might use semantic editor combinators (SECs), as Lambdageek suggested.
First a couple of helpful abbreviations:
type Unop a = a -> a
type Lifter p q = Unop p -> Unop q
The Unop
here is an "semantic editor", and the Lifter
is the semantic editor combinator.
Some lifters:
onManager :: Lifter Coach BBTeam
onManager f (BBTeam n m p) = BBTeam n (f m) p
onDiet :: Lifter Diet Coach
onDiet f (Coach n c d) = Coach n c (f d)
onStakes :: Lifter Integer Diet
onStakes f (Diet n s e) = Diet n (f s) e
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):
addManagerSteak :: Unop BBTeam
addManagerSteak = (onManager . onDiet . onStakes) (+1)
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:
onManager :: Lifter Coach BBTeam
onManager f t = t { manager = f (manager t) }
Solution 3:
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:
Consider the following data types that describe the organisational structure of a company. A company is divided into departments. Each department has a manager, and consists of a collection of sub-units, where a unit is either a single employee or a department. Both managers and ordinary employees are just persons receiving a salary.
[skiped]
Now suppose we want to increase the salary of everyone in the company by a specified percentage. That is, we must write the function:
increase :: Float -> Company -> Company
(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.
{-# LANGUAGE DeriveDataTypeable #-}
import Data.Generics
data BBTeam = BBTeam { teamname :: String,
manager :: Coach,
players :: [BBPlayer]} deriving (Show, Data, Typeable)
data Coach = Coach { coachname :: String,
favcussword :: String,
diet :: Diet } deriving (Show, Data, Typeable)
data Diet = Diet { dietname :: String,
steaks :: Integer,
eggs :: Integer} deriving (Show, Data, Typeable)
data BBPlayer = BBPlayer { playername :: String,
hits :: Integer,
era :: Double } deriving (Show, Data, Typeable)
incS d@(Diet _ s _) = d { steaks = s+1 }
addManagerSteak :: BBTeam -> BBTeam
addManagerSteak = everywhere (mkT incS)