¿Hay un modismo de Haskell para actualizar una estructura de datos anidada?

Digamos que tengo el siguiente modelo de datos, para hacer un seguimiento de las estadísticas de los jugadores de béisbol, equipos y entrenadores:

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) 

Ahora digamos que los gerentes, que generalmente son fanáticos de la carne, quieren comer aún más bistec, por lo que debemos ser capaces de boost el contenido de bistec de la dieta de un gerente. Aquí hay dos posibles implementaciones para esta función:

1) Esto usa mucha coincidencia de patrones y tengo que obtener todo el orden de los argumentos para todos los constructores bien … dos veces. Parece que no se escalaría muy bien o sería muy fácil de mantener / leer.

 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) Esto utiliza todos los accesos provistos por la syntax de registro de Haskell, pero también es feo y repetitivo, y es difícil de mantener y leer, creo.

 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 

Mi pregunta es, ¿es uno de estos mejor que el otro, o más preferido dentro de la comunidad Haskell? ¿Hay una mejor manera de hacer esto (modificar un valor profundo dentro de una estructura de datos mientras se mantiene el contexto)? No me preocupa la eficiencia, solo código elegancia / generalidad / mantenibilidad.

Me di cuenta de que hay algo para este problema (¿o un problema similar?) En Clojure: update-in , así que creo que estoy tratando de entender la update-in en el contexto de la progtwigción funcional y Haskell y el tipado estático.

    La syntax de la actualización de registros viene estándar con el comstackdor:

     addManStk team = team { manager = (manager team) { diet = (diet (manager team)) { steaks = steaks (diet (manager team)) + 1 } } } 

    ¡Terrible! Pero hay una mejor manera. Hay varios paquetes en Hackage que implementan referencias funcionales y lentes, que es definitivamente lo que quieres hacer. Por ejemplo, con el paquete fclabels , pondría guiones bajos delante de todos sus nombres de registro, luego escribiría

     $(mkLabels ['BBTeam, 'Coach, 'Diet, 'BBPlayer]) addManStk = modify (+1) (steaks . diet . manager) 

    Editado en 2017 para agregar: en la actualidad existe un amplio consenso en cuanto a que el paquete de lentes es una técnica de implementación particularmente buena. Si bien es un paquete muy grande, también hay muy buena documentación y material introductorio disponible en varios lugares de la web.

    Aquí se explica cómo puede usar los combinadores de editor semántico (SEC), como sugirió Lambdageek.

    Primero un par de abreviaciones útiles:

     type Unop a = a -> a type Lifter pq = Unop p -> Unop q 

    El Unop aquí es un “editor semántico”, y el Lifter es el combinador de editor semántico. Algunos levantadores:

     onManager :: Lifter Coach BBTeam onManager f (BBTeam nmp) = BBTeam n (fm) p onDiet :: Lifter Diet Coach onDiet f (Coach ncd) = Coach nc (fd) onStakes :: Lifter Integer Diet onStakes f (Diet nse) = Diet n (fs) e 

    Ahora simplemente redacte las SEC para decir lo que quiera, es decir, agregue 1 a las apuestas de la dieta del gerente (de un equipo):

     addManagerSteak :: Unop BBTeam addManagerSteak = (onManager . onDiet . onStakes) (+1) 

    Comparando con el enfoque SYB, la versión SEC requiere un trabajo adicional para definir los SEC, y solo proporcioné los necesarios en este ejemplo. La SEC permite la aplicación dirigida, lo que sería útil si los jugadores tuvieran dietas pero no quisiéramos ajustarlos. Tal vez haya una forma bonita de SYB para manejar esa distinción también.

    Editar: Aquí hay un estilo alternativo para los SEC básicos:

     onManager :: Lifter Coach BBTeam onManager ft = t { manager = f (manager t) } 

    Más adelante, también puede consultar algunas bibliotecas genéricas de progtwigción: cuando aumenta la complejidad de sus datos y se encuentra escribiendo más y un código repetitivo (como boost el contenido de bistec para los jugadores, las dietas de los entrenadores y el contenido de cerveza de los observadores). sigue siendo repetitivo incluso en forma menos detallada. SYB es probablemente la biblioteca más conocida (y viene con la plataforma Haskell). De hecho, el documento original sobre SYB usa un problema muy similar para demostrar el enfoque:

    Considere los siguientes tipos de datos que describen la estructura organizativa de una empresa. Una empresa está dividida en departamentos. Cada departamento tiene un administrador, y consiste en una colección de subunidades, donde una unidad es un solo empleado o un departamento. Tanto los gerentes como los empleados comunes son solo personas que reciben un salario.

    [esquiada]

    Ahora supongamos que queremos boost el salario de todos en la empresa en un porcentaje específico. Es decir, debemos escribir la función:

    boost :: Flotar -> Empresa -> Empresa

    (el rest está en el papel – se recomienda leer)

    Por supuesto, en su ejemplo solo necesita acceder / modificar una parte de una estructura de datos pequeña para que no requiera un enfoque genérico (sigue siendo la solución basada en SYB para su tarea) pero una vez que ve el código / patrón de acceso repetitivo / modificación que desea verificar esta u otras bibliotecas de progtwigción genéricas.

     {-# 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)