Существует ли идиома Haskell для обновления вложенной структуры данных? - PullRequest
41 голосов
/ 09 сентября 2011

Допустим, у меня есть следующая модель данных для отслеживания статистики бейсболистов, команд и тренеров:

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)

Теперь давайте скажем, что менеджеры, которые обычно являются фанатиками стейков, хотят есть еще больше стейков - поэтому мы должны иметь возможность увеличить содержание стейков в диете менеджера. Вот две возможные реализации этой функции:

1) При этом используется множество сопоставлений с образцом, и мне нужно правильно упорядочить все аргументы для всех конструкторов ... дважды. Похоже, что он не будет хорошо масштабироваться или будет очень удобен в обслуживании / читабельности.

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) При этом используются все методы доступа, предоставляемые синтаксисом записей Haskell, но он также уродлив и повторяется, и, я думаю, его трудно поддерживать и читать.

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

Мой вопрос: один из них лучше другого или более предпочтителен в сообществе Haskell? Есть ли лучший способ сделать это (изменить значение глубоко внутри структуры данных, сохраняя при этом контекст)? Я не беспокоюсь об эффективности, просто элегантность кода / универсальность / ремонтопригодность.

Я заметил, что в Clojure есть что-то для этой проблемы (или аналогичной проблемы?): update-in - так что я думаю, что я пытаюсь понять update-in в контексте функционального программирования и Haskell и статической типизации .

Ответы [ 3 ]

36 голосов
/ 09 сентября 2011

Синтаксис обновления записи стандартно поставляется с компилятором:

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

Ужасно!Но есть и лучший способ.В Hackage есть несколько пакетов, которые реализуют функциональные ссылки и линзы, и это определенно то, что вы хотите сделать.Например, с пакетом fclabels вы должны поставить подчеркивание перед всеми именами ваших записей, а затем написать

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

Отредактировано в 2017 году, чтобы добавить: в эти дни существует широкий консенсус попакет lens является особенно хорошей техникой реализации.Несмотря на то, что это очень большой пакет, есть также очень хорошая документация и вводный материал, доступный в различных местах в Интернете.

9 голосов
/ 24 декабря 2011

Вот как вы можете использовать комбинаторы семантического редактора (SEC), как предложил Lambdageek.

Сначала пара полезных сокращений:

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

Unop здесь - это "семантика"редактор ", а Lifter является комбинатором семантического редактора.Некоторые спортсмены:

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

Теперь просто составьте SEC, чтобы сказать, что вы хотите, а именно добавьте 1 к ставкам диеты менеджера (команды):

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

По сравнению с подходом SYB, версия SEC требует дополнительной работы для определения SEC, и я предоставил только те, которые необходимы в этом примере.SEC позволяет целевое приложение, которое было бы полезно, если бы у игроков были диеты, но мы не хотели их настраивать.Возможно, есть и симпатичный способ SYB для обработки этого различия.

Редактировать: Вот альтернативный стиль для базовых SEC:

onManager :: Lifter Coach BBTeam
onManager f t = t { manager = f (manager t) }
5 голосов
/ 10 сентября 2011

Позже вы, возможно, захотите взглянуть на некоторые универсальные библиотеки программирования: когда сложность ваших данных возрастает, и вы обнаруживаете, что пишете больше и шаблонный код (например, увеличение содержания стейков для игроков, диет тренеров и пива) наблюдатели), который до сих пор является образцом даже в менее многословной форме. SYB , пожалуй, самая известная библиотека (и поставляется с платформой Haskell). На самом деле оригинальная статья на SYB использует очень похожую проблему для демонстрации подхода:

Рассмотрим следующие типы данных, которые описывают организационную структуру компании. Компания делится на отделы. В каждом отделе есть менеджер, и он состоит из набора подразделений, где подразделение является либо отдельным сотрудником, либо отделом. И менеджеры, и обычные работники - это просто лица, получающие зарплату.

[пропустить,]

Теперь предположим, что мы хотим увеличить зарплату всем сотрудникам компании на определенный процент. То есть мы должны написать функцию:

увеличение :: Float -> Компания -> Компания

(остальное в статье - рекомендуется чтение)

Конечно, в вашем примере вам просто нужно получить доступ / изменить один фрагмент крошечной структуры данных, чтобы он не требовал универсального подхода (все же решение на основе SYB для вашей задачи приведено ниже), но как только вы увидите повторяющийся код / ​​шаблон доступа или модификации вы можете проверить эту или другие универсальные библиотеки программирования.

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