Подтвердить что ты не робот

Существует ли игла Haskell для обновления вложенной структуры данных?

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

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 и статическая типизация.

4b9b3361

Ответ 1

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

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, являющегося особенно хорошим методом реализации. Хотя это очень большой пакет, есть также очень хорошая документация и вводные материалы, доступные в разных местах в Интернете.

Ответ 2

Вот как вы могли бы использовать комбинаторы семантического редактора (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) }

Ответ 3

Позже вы также можете взглянуть на некоторые общие библиотеки программирования: когда сложность ваших данных увеличивается, и вы обнаруживаете, что пишете больше и код шаблона (например, увеличиваете содержание стейков для игроков, диеты тренеров и содержание пива в наблюдателей), который по-прежнему является шаблоном даже в менее сложной форме. 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 [email protected](Diet _ s _) = d { steaks = s+1 }

addManagerSteak :: BBTeam -> BBTeam
addManagerSteak = everywhere (mkT incS)