Синтаксис записи в 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 определено несколько раз. И я не вижу способа сделать их частью класса типов, чтобы я мог передать их в modifyX.
Есть ли хороший чистый способ решения этой проблемы, или я застрял в определении своих собственных методов получения и установки? Другими словами, есть ли способ соединить функции, созданные синтаксисом записи, с классами типов (как геттерами, так и сеттерами)?
РЕДАКТИРОВАТЬ
Вот настоящая проблема, которую я пытаюсь решить. Я пишу серию связанных программ, которые все используют System.Console.GetOpt для анализа параметров командной строки. Там будет много параметров командной строки, которые являются общими для этих программ, но некоторые программы могут иметь дополнительные параметры. Мне бы хотелось, чтобы каждая программа могла определять запись, содержащую все значения ее параметров. Затем я начинаю со значения записи по умолчанию, которое затем преобразуется через монаду StateT и GetOpt, чтобы получить окончательную запись, отражающую аргументы командной строки. Для одной программы этот подход работает очень хорошо, но я пытаюсь найти способ повторно использовать код во всех программах.
4 ответа
Вам нужны расширяемые записи, которые, как я понимаю, являются одной из самых обсуждаемых тем в Haskell. Похоже, что в настоящее время нет единого мнения о том, как его реализовать.
В вашем случае кажется, что, возможно, вместо обычной записи вы могли бы использовать гетерогенный список, подобный тем, которые реализованы в HList.
Опять же, кажется, у вас есть только два уровня: общий и программный. Поэтому, возможно, вам следует просто определить общий тип записи для общих параметров и тип записи для каждой программы и использовать StateT в кортеже этих типов. Для общих вещей вы можете добавить псевдонимы, которые составляют fst
с общими средствами доступа, поэтому он невидим для звонящих.
Вы можете использовать такой код, как
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' }
Что вы моделируете в своем коде? Если бы мы знали больше о проблеме, мы могли бы предложить что-то менее неловкое, чем этот объектно-ориентированный дизайн, включенный в функциональный язык.
Сдается мне как работа для дженериков. Если бы вы могли пометить свой Int разными newtypes, то вы могли бы написать (с помощью uniplate, модуля 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 ]
Это извлекло бы все чье-либо из любого места внутри Opts.
Модификация также возможна.
Если вы сделаете экземпляры типов Foldable, вы получите функцию toList, которую вы можете использовать в качестве основы вашего метода доступа.
Если Foldable ничего не делает с вами, тогда, возможно, правильный подход - определить интерфейс, который вы хотите, как класс типов, и найти хороший способ автоматической генерации производных значений.
Возможно, исходя из выполнения
deriving(Data)
Вы можете использовать gmap-комбинаторы, чтобы отключить доступ.