Existe uma linguagem Haskell para atualizar uma estrutura de dados aninhada?

Digamos que eu tenha o seguinte modelo de dados, para acompanhar as statistics de jogadores de beisebol, equipes e treinadores:

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) 

Agora, digamos que os gerentes, que geralmente são fanáticos por bife, queiram comer ainda mais bife – então precisamos aumentar o conteúdo de bife da dieta de um gerente. Aqui estão duas implementações possíveis para esta function:

1) Isso usa muita correspondência de padrões e eu tenho que obter todas as ordenações de argumentos para todos os construtores certas … duas vezes. Parece que não vai escalar muito bem ou ser muito sustentável / legível.

 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) Isso usa todos os acessadores fornecidos pela syntax de registro do Haskell, mas também é feio e repetitivo, e difícil de manter e ler, eu acho.

 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 

Minha pergunta é, um desses é melhor que o outro, ou mais preferido dentro da comunidade Haskell? Existe uma maneira melhor de fazer isso (para modificar um valor dentro de uma estrutura de dados enquanto mantém o contexto)? Não estou preocupado com eficiência, apenas codifique elegância / generalidade / manutenção.

Eu notei que há algo para esse problema (ou um problema semelhante?) No Clojure: update-in – então eu acho que estou tentando entender o update-in no contexto da functional programming e Haskell e tipagem estática.

A syntax de atualização de registro vem por padrão com o compilador:

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

Terrivel! Mas há um jeito melhor. Existem vários pacotes no Hackage que implementam referências funcionais e lentes, que é definitivamente o que você quer fazer. Por exemplo, com o pacote fclabels , você colocaria sublinhados na frente de todos os nomes dos seus registros e, em seguida, escreveria

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

Editado em 2017 para adicionar: estes dias há um amplo consenso sobre o pacote de lentes sendo uma técnica de implementação particularmente boa. Embora seja um pacote muito grande, também há documentação e material introdutório muito bons disponíveis em vários locais da web.

Veja como você pode usar os combinadores de edição semântica (SECs), como o Lambdageek sugeriu.

Primeiro algumas abreviaturas úteis:

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

O Unop aqui é um “editor semântico” e o Lifter é o combinador do editor semântico. Alguns 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 

Agora, basta compor as SECs para dizer o que você quer, ou seja, adicionar 1 às apostas da dieta do gerente (de uma equipe):

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

Comparando com a abordagem SYB, a versão SEC requer trabalho extra para definir os SECs, e eu só forneci os necessários neste exemplo. A SEC permite a aplicação direcionada, o que seria útil se os jogadores tivessem dietas, mas não quiséssemos ajustá-las. Talvez haja uma maneira bonita de lidar com essa distinção também.

Edit: Aqui está um estilo alternativo para as SECs básicas:

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

Mais tarde, você também pode dar uma olhada em algumas bibliotecas de programação genéricas: quando a complexidade de seus dados aumenta e você se encontra escrevendo mais código clichê (como aumentar o conteúdo de bife para os jogadores, as dietas dos treinadores e o conteúdo de cerveja dos observadores) ainda é clichê, mesmo em forma menos detalhada. O SYB é provavelmente a biblioteca mais conhecida (e vem com o Haskell Platform). Na verdade, o artigo original sobre o SYB usa um problema muito semelhante para demonstrar a abordagem:

Considere os seguintes tipos de dados que descrevem a estrutura organizacional de uma empresa. Uma empresa é dividida em departamentos. Cada departamento possui um gerente e consiste em uma coleção de subunidades, em que uma unidade é um único funcionário ou um departamento. Tanto os gerentes quanto os funcionários comuns são apenas pessoas que recebem um salário.

[skiped]

Agora, suponha que queremos aumentar o salário de todos na empresa em uma porcentagem específica. Ou seja, devemos escrever a function:

aumentar :: Float -> Empresa -> Empresa

(o resto está no jornal – a leitura é recomendada)

Claro que no seu exemplo você só precisa acessar / modificar uma parte de uma pequena estrutura de dados para que não exija uma abordagem genérica (ainda que a solução baseada em SYB para sua tarefa esteja abaixo), mas uma vez você vê repetindo código / padrão de access / modificação você meu quer verificar esta ou outras bibliotecas de programação 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)