haskellfunctional-programmingclojurestatic-typing

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

  • 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.