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