50

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.

amindfv
  • 8,438
  • 5
  • 36
  • 58
Matt Fenwick
  • 48,199
  • 22
  • 128
  • 192
  • 2
    I don't believe your style conventions are helping make Haskell seem approachable.. :( – ChaosPandion Sep 09 '11 at 17:47
  • 3
    @ChaosPandion -- I'm not an advanced Haskeller, I would definitely appreciate suggestions for improving .... it is a pretty long post, I spent a long time on it and I wish I could have made it more terse and to the point – Matt Fenwick Sep 09 '11 at 17:48
  • Unfortunately I am relatively new to Haskell myself. I have a way more experience with F#. – ChaosPandion Sep 09 '11 at 17:55
  • 3
    Something like this http://conal.net/blog/posts/semantic-editor-combinators may help. – Lambdageek Sep 09 '11 at 17:58
  • 1
    see also http://stackoverflow.com/questions/5767129/lenses-fclabels-data-accessor-which-library-for-structure-access-and-mutation – hvr Sep 09 '11 at 19:38

4 Answers4

43

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.

Daniel Wagner
  • 145,880
  • 9
  • 220
  • 380
  • Thanks! Just what I was looking for! Could you explain more about lenses? Is that the functional concept that I'm grasping at? – Matt Fenwick Sep 09 '11 at 18:10
  • 1
    @Matt Fenwick: There's not much else to explain about the concept; ignoring implementation issues and performance, they're pretty much about doing exactly what you wanted to do. The simplest implementation is just a pair of functions; one that extracts a piece from something bigger, and one that replaces the same piece. The only trick here is that you can compose pairs of such functions by creating a pair of composed functions, which might be the functional concept you weren't seeing. – C. A. McCann Sep 09 '11 at 18:24
  • 7
    @Matt Fenwick [Overloading functional references](http://twanvl.nl/blog/haskell/overloading-functional-references) is one of my top 10 favorite pieces of Haskell writing. – Daniel Wagner Sep 09 '11 at 18:29
  • 1
    The example of record syntax is incorrect and will not compile. Because the record update binds more tightly than function application, you need `manager = (manager team) { diet = (diet (manager team)) { steaks = steaks (diet (manager team)) + 1 }}` – massysett Feb 12 '14 at 20:04
  • @OmariNorman You're absolutely right, thanks for pointing that out. I'll edit shortly! – Daniel Wagner Feb 12 '14 at 20:08
  • Claiming there is “broad consensus” is a harmful distortion of reality. In reality, it’s more like a cult of sunken cost fallacy. Because like with any cult, one has to completely buy into it, and then justifies that with it having to be good if one wasted so much effort on in. (Compare: Apple products)… In reality, is is a massive and very ugly hack with incomprehensible error messages, and the thing to fix is the language syntax itself. Which has now been updated with [record dot syntax](https://github.com/ghc-proposals/ghc-proposals/blob/master/proposals/0282-record-dot-syntax.rst) – Evi1M4chine Jul 25 '23 at 14:53
11

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) }
Conal
  • 18,517
  • 2
  • 37
  • 40
5

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)
Community
  • 1
  • 1
Ed'ka
  • 6,595
  • 29
  • 30
0

The modern solution is record dot syntax, which has been slowly added as compiler extensions over the last years.

Currently, you need GHC 9.2 do it like this:

{-# LANGUAGE OverloadedRecordDot, OverloadedRecordUpdate #-}

addManagerSteak team = team { manager.diet.steaks = team.manager.diet.steaks + 1 }

You could also use the new pattern matching syntax (NamedFieldPuns and RecordWildCards) to match on steaks instead of team, and use that instead of team.manager.diet.steaks, but it would be longer.

Also, succ is a more universal function than + 1 And in Haskell, all variables are constants, so a straight-up += syntax would be misleading.

So the other ways, especially the lens package (which has always been an ugly hack that tried to solve the problem at the wrong place (as a library instead of fixing the language syntax) out of simple necessity, and, if anything went wrong, gave incomprehensible error messages as a result of that) are considered outdated.
You can even disable the old built-in syntax with {-# LANGUAGE NoTraditionalRecordSyntax #-}.

Evi1M4chine
  • 6,992
  • 1
  • 24
  • 18