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

Синтаксис записей и типы классов Haskell

Предположим, что у меня есть два типа данных Foo и Bar. Foo имеет поля x и y. Бар имеет поля x и z. Я хочу, чтобы иметь возможность написать функцию, которая принимает либо Foo, либо Bar в качестве параметра, извлекает значение x, выполняет некоторые вычисления на нем, а затем возвращает новый Foo или Bar с установленным значением x.

Вот один из подходов:

class HasX a where
    getX :: a -> Int
    setX :: a -> Int -> a

data Foo = Foo Int Int deriving Show

instance HasX Foo where
    getX (Foo x _) = x
    setX (Foo _ y) val = Foo val y

getY (Foo _ z) = z
setY (Foo x _) val = Foo x val

data Bar = Bar Int Int deriving Show

instance HasX Bar where
    getX (Bar x _) = x
    setX (Bar _ z) val = Bar val z

getZ (Bar _ z) = z
setZ (Bar x _) val = Bar x val

modifyX :: (HasX a) => a -> a
modifyX hasX = setX hasX $ getX hasX + 5

Проблема заключается в том, что все те геттеры и сеттеры очень больны, особенно если я заменяю Foo и Bar реальными типами данных, у которых много полей.

Синтаксис записи Haskell дает гораздо лучший способ определения этих записей. Но, если я попытаюсь определить записи, подобные этому

data Foo = Foo {x :: Int, y :: Int} deriving Show
data Bar = Foo {x :: Int, z :: Int} deriving Show

Я получу ошибку, говоря, что x определяется несколько раз. И я не вижу никакого способа сделать эту часть класса типа, чтобы я мог передать их для измененияX.

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

ИЗМЕНИТЬ

Вот реальная проблема, которую я пытаюсь решить. Я пишу серию связанных программ, которые используют System.Console.GetOpt для анализа своих параметров командной строки. В этих программах будет много опций командной строки, но некоторые из них могут иметь дополнительные опции. Я хочу, чтобы каждая программа могла определять запись, содержащую все ее значения параметров. Затем я начинаю с значения записи по умолчанию, которое затем преобразуется через монаду StateT и GetOpt, чтобы получить окончательную запись, отражающую аргументы командной строки. Для одной программы этот подход работает очень хорошо, но я пытаюсь найти способ повторного использования кода во всех программах.

4b9b3361

Ответ 1

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

В вашем случае кажется, что вместо обычной записи вы можете использовать гетерогенный список, подобный тем, которые реализованы в HList.

И снова, похоже, у вас есть только два уровня: общий и программный. Поэтому, возможно, вам нужно просто определить общий тип записи для общих опций и тип записи для конкретной программы для каждой программы и использовать StateT в кортеже этих типов. Для обычных вещей вы можете добавлять псевдонимы, которые составляют fst с общими аксессуарами, поэтому они не видны для вызывающих.

Ответ 2

Вы можете использовать код, например

data Foo = Foo { fooX :: Int, fooY :: Int } deriving (Show)
data Bar = Bar { barX :: Int, barZ :: Int } deriving (Show)

instance HasX Foo where
  getX = fooX
  setX r x' = r { fooX = x' }

instance HasX Bar where
  getX = barX
  setX r x' = r { barX = x' }

Что вы моделируете в своем коде? Если бы мы знали больше о проблеме, мы могли бы предложить что-то менее неудобное, чем это объектно-ориентированное проектирование, которое было бы включено в функциональный язык.

Ответ 3

Мне кажется, как работа для дженериков. Если вы можете пометить свой Int с помощью новых типов, то вы сможете написать (с помощью uniplate, module PlateData):

data Foo = Foo Something Another deriving (Data,Typeable)
data Bar = Bar Another Thing deriving (Data, Typerable)

data Opts = F Foo | B Bar

newtype Something = S Int
newtype Another = A Int
newtype Thing = T Int

getAnothers opts = [ x | A x <- universeBi opts ]

Это приведет к извлечению всего другого из любой точки внутри Opt.

Модификация возможна также.

Ответ 4

Если вы делаете экземпляры типов Foldable, вы получаете функцию toList, которую вы можете использовать в качестве основы для вашего аксессора.

Если Foldable ничего вам не делает, то, возможно, правильный подход заключается в том, чтобы определить интерфейс, который вы хотите, как класс типа, и найти хороший способ автогенерации полученных значений.

Возможно, из-за выполнения

deriving(Data)

вы можете использовать комбинаторы gmap, чтобы отключить доступ.