Pregunta

Digamos que tengo el siguiente modelo de datos, para realizar un seguimiento de las estadísticas de 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 del bistec, quieren comer aún más bistec, por lo que necesitamos poder aumentar el contenido de bistec de la dieta de un gerente. Aquí hay dos posibles implementaciones para esta función:

1) Esto utiliza mucha coincidencia de patrones y tengo que obtener todos los argumentos que ordenan para todos los constructores correctos ... dos veces. Parece que no escalaría muy bien o sería muy mantenible/legible.

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 accesorios proporcionados por la sintaxis discográfica de Haskell, pero también es feo y repetitivo, y 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 de Haskell? ¿Existe una mejor manera de hacer esto (para modificar un valor en el fondo de una estructura de datos mientras mantiene el contexto)? No me preocupa la eficiencia, solo el código elegancia/generalidad/mantenibilidad.

Noté que hay algo para este problema (¿o un problema similar?) En Clojure: update-in - Así que creo que estoy tratando de entender update-in En el contexto de la programación funcional y la tipificación de Haskell y estática.

¿Fue útil?

Solución

La sintaxis de actualización de registro viene de serie con el compilador:

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

¡Horrible! Pero hay una mejor manera. Hay varios paquetes en hackaje que implementan referencias y lentes funcionales, que definitivamente es lo que desea hacer. Por ejemplo, con el fclabels paquete, pondrías subrayadores frente a todos tus nombres de registro, luego escribirías

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

Editado en 2017 para agregar: En estos días hay un amplio consenso sobre el lente El paquete 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.

Otros consejos

Así es como podría usar los combinadores de editores semánticos (SEC), como sugirió Lambdageek.

Primero un par de abreviaturas útiles:

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

los Unop Aquí hay un "editor semántico", y el Lifter es el editor semántico combinador. Algunos levantadores:

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

Ahora simplemente componga las SEC para decir lo que desea, a saber, Agregue 1 a las apuestas de la dieta del gerente (de un equipo):

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

En comparación con el enfoque SYB, la versión SEC requiere un trabajo adicional para definir el SEC, y solo he proporcionado los necesarios en este ejemplo. La SEC permite la aplicación dirigida, lo que sería útil si los jugadores tuvieran dietas pero no queríamos ajustarlas. Tal vez hay una bonita forma de SYB de manejar esa distinción también.

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

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

Más tarde, también es posible que desee echar un vistazo a algunas bibliotecas de programación genérica: cuando aumenta la complejidad de sus datos y se encuentra escribiendo más y código básico (como aumentar el contenido de carne para los jugadores, las dietas de los entrenadores y el contenido de la cerveza de los observadores) que sigue siendo horario incluso en forma menos detallada.Syb es probablemente la biblioteca más conocida (y viene con la plataforma Haskell). De hecho el Documento original en SYB Utiliza un problema muy similar para demostrar el enfoque:

Considere los siguientes tipos de datos que describen la estructura organizativa de una empresa. Una empresa se divide en departamentos. Cada departamento tiene un gerente 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.

Salto

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

aumentar :: float -> empresa -> empresa

(El resto está en el papel: se recomienda la lectura)

Por supuesto, en su ejemplo, solo necesita acceder/modificar una pieza de una pequeña estructura de datos para que no requiere un enfoque genérico (aún así, la solución basada en SYB para su tarea está a continuación), pero una vez que vea el código de repetición/patrón de acceso/ modificación de usted mi deseo de verificar esto o otro Bibliotecas de programación genérica.

{-# 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)
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top