2010-04-13 7 views
13

Rozważmy typ danych z wielu konstruktorów:„dopasowanie Wzór” konstruktorów danych algebraicznych typu

data T = Alpha Int | Beta Int | Gamma Int Int | Delta Int 

Chcę napisać funkcję, aby sprawdzić, czy dwie wartości są produkowane z tego samego konstruktora:

sameK (Alpha _) (Alpha _) = True 
sameK (Beta _) (Beta _) = True 
sameK (Gamma _ _) (Gamma _ _) = True 
sameK _ _ = False 

Utrzymanie sameK nie jest zabawne, nie można go łatwo sprawdzić pod kątem poprawności. Na przykład, gdy nowe konstruktory są dodawane do T, łatwo zapomnieć o aktualizacji sameK. Pominąłem jedną linię, aby dać przykład:

-- it’s easy to forget: 
-- sameK (Delta _) (Delta _) = True 

Powstaje pytanie, jak uniknąć boilerplate w sameK? Lub jak upewnić się, że sprawdza dla wszystkich konstruktorów T?


Obejście znalazłem jest stosowanie oddzielnych typów danych dla każdego z wykonawców, wynikające Data.Typeable i deklarując wspólną klasę typu, ale nie podoba mi się to rozwiązanie, ponieważ jest o wiele mniej czytelny i w przeciwnym razie tylko proste algebraiczne typu działa na mnie:

{-# LANGUAGE DeriveDataTypeable #-} 

import Data.Typeable 

class Tlike t where 
    value :: t -> t 
    value = id 

data Alpha = Alpha Int deriving Typeable 
data Beta = Beta Int deriving Typeable 
data Gamma = Gamma Int Int deriving Typeable 
data Delta = Delta Int deriving Typeable 

instance Tlike Alpha 
instance Tlike Beta 
instance Tlike Gamma 
instance Tlike Delta 

sameK :: (Tlike t, Typeable t, Tlike t', Typeable t') => t -> t' -> Bool 
sameK a b = typeOf a == typeOf b 
+0

Dziękuję wszystkim za odpowiedź. Wszystkie twoje odpowiedzi były użyteczne. – sastanin

Odpowiedz

10

Zobacz w szczególności moduł Data.Data, funkcja toConstr. Wraz z {-# LANGUAGE DeriveDataTypeable #-} otrzymasz rozwiązanie 1-liniowe, które działa dla dowolnego typu, który jest instancją Data.Data. Nie musisz wymyślać wszystkich SYB!

Jeśli z jakiegoś powodu (utknął z Uściski?), To nie jest opcja, to tutaj jest bardzo brzydki i bardzo wolny hack. Działa tylko wtedy, gdy twój typ danych jest w stanie (np. Za pomocą deriving (Show) - na przykład brak typów funkcji w środku).

constrT :: T -> String 
constrT = head . words . show 
sameK x y = constrT x == constrT y 

constrT pobiera ciąg znaków reprezentujący zewnętrznej konstruktora wartości T pokazując go, siekanie go na słowach, a następnie uzyskanie pierwszego. Podaję wyraźny podpis, więc nie masz ochoty używać go na innych typach (i unikać ograniczenia monomorfizmu).

Niektóre znaczące wady:

  • To łamie potwornie gdy typ ma konstruktorów Infix (takie jak data T2 = Eta Int | T2 :^: T2)
  • W przypadku niektórych swoich konstruktorów mają wspólny przedrostek, to będzie się wolniej, ponieważ większa część strun musi być porównywana.
  • Nie działa dla typów z niestandardowymi show, takich jak wiele typów bibliotek.

Powiedział, że jest Haskell 98 ... ale to tylko miły co mogę powiedzieć o tym!

+1

+1 dla' toConstr' –

+1

Wow! To tylko magia. Wielkie dzięki! – sastanin

14

Innym możliwym sposobem:

sameK x y = f x == f y 
    where f (Alpha _) = 0 
     f (Beta _) = 1 
     f (Gamma _ _) = 2 
     -- runtime error when Delta value encountered 

Runtime error nie jest idealne, ale lepsze niż dyskretnie daje złą odpowiedź.

+3

Podoba mi się. GHC z '-Wall' uprzejmie zgłasza nieumyślne dopasowania wzorców w definicji' f ', więc można zapobiec błędowi w czasie wykonywania. Dziękuję za pomysł. – sastanin

10

Aby to zrobić, musisz użyć biblioteki generycznej, takiej jak Scrap Your Boilerplate lub uniplate.

Jeśli nie chcesz być tak brutalne, można użyć roztworu Dave Hinton, wraz z pustego rekordu skrótu:

... 
where f (Alpha {}) = 0 
     f (Beta {}) = 1 
     f (Gamma {}) = 2 

Więc nie trzeba wiedzieć, jak wiele każdy args konstruktor ma. Ale oczywiście nadal pozostawia wiele do życzenia.

+2

Dobra sztuczka z '{}'. Nie wiedziałem o tym. Dziękuję Ci. – sastanin

+2

Klamry zapisu wiążą się silniej niż cokolwiek innego, więc z technicznego punktu widzenia nie potrzebujesz nawiasów. 'f Alpha {} = 0' będzie działało dobrze, chociaż nie jestem pewien, jak łatwo jest to odczytać, wygląda na to, że' f' przyjmuje dwa argumenty. Czasami zajadam się 'f Alpha {} = 0' ... –

1

Z pewnością można użyć generycznych, aby wyeliminować blankiet. Twój kod jest przykładem podręcznika, dlaczego ja (i wiele innych nigdy nie używam używać symbolu wieloznacznego _ na najwyższym poziomie). Chociaż nudne jest zapisywanie wszystkich przypadków, jest to mniej uciążliwe niż rozwiązywanie problemów.

W tym szczęśliwym przykładzie użyłbym nie tylko rozwiązania Dave'a Hintona, ale uderzyłbym w pragmat INLINE w funkcję pomocniczą f.

+0

Tak, dlatego proszę. Dziękuję za radę. – sastanin

Powiązane problemy