Składnia rekordów Haskella i klasy typów
Załóżmy, że mam dwa typy danych Foo i Bar. Foo ma pola x i y. Pasek ma pola x i z. Chcę móc napisać funkcję, która pobiera parametr Foo lub Bar, wyodrębnia wartość x, wykonuje na niej obliczenia, a następnie zwraca nową wartość Foo lub Bar z odpowiednio ustawioną wartością x.
Oto jedno podejście:
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
Problem polega na tym, że wszystkie te programy pobierające i ustawiające są bolesne do napisania, zwłaszcza jeśli zastąpię Foo i Bar typami danych rzeczywistych, które mają wiele pól.
Składnia rekordów Haskella daje o wiele ładniejszy sposób definiowania tych rekordów. Ale jeśli spróbuję zdefiniować takie rekordy
data Foo = Foo {x :: Int, y :: Int} deriving Show
data Bar = Foo {x :: Int, z :: Int} deriving Show
Pojawi się błąd mówiący, że x jest zdefiniowany wiele razy. I nie widzę żadnego sposobu, aby uczynić te części klasy typu, aby móc przekazać je do modyfikacjiX.
Czy istnieje miły, czysty sposób rozwiązania tego problemu, czy też utknąłem przy definiowaniu własnych getterów i setterów? Innymi słowy, czy istnieje sposób łączenia funkcji utworzonych przez składnię rekordów z klasami typów (zarówno pobierającymi, jak i ustawiającymi)?
EDYTOWAĆ
Oto prawdziwy problem, który próbuję rozwiązać. Piszę serię powiązanych programów, z których wszystkie używają System.Console.GetOpt do analizowania swoich opcji wiersza poleceń. Będzie wiele opcji wiersza polecenia, które są wspólne dla tych programów, ale niektóre programy mogą mieć dodatkowe opcje. Chciałbym, aby każdy program mógł zdefiniować rekord zawierający wszystkie jego wartości opcji. Następnie zaczynam od domyślnej wartości rekordu, która jest następnie przekształcana przez monadę StateT i GetOpt, aby uzyskać ostatni rekord odzwierciedlający argumenty wiersza polecenia. W przypadku pojedynczego programu podejście to działa naprawdę dobrze, ale staram się znaleźć sposób na ponowne użycie kodu we wszystkich programach.