2016-11-02 6 views
3

Ich möchte eine Reihe von Tests in einem einfachen Demo-Programm definieren, wo jeder Test lokal definiert ist, aber alle können an einem Standardplatz gedruckt werden.Wie spezifiziert man einen Typ mit Einschränkungen?

Zum Beispiel;

t1 = ("Sqrt(4)", sqrt(4.0)) 
... 
t2 = ("sumList:", sum [1,2,3,4]) 
... 
t3 = ("Description", value) 
... 

Also jeder Test ist vom Typ: (String, value), für verschiedene Werttypen von denen alle (nur) haben Mitglieder der anzeigen Klasse.

Dann für die Zusammenfassung der Tests, eine Schleife:

test (msg, val) = do print $ msg ++ " :: " ++ show val 
tests ts  = mapM test ts 

Dies kompiliert, und ordnet diese Typen:

test :: Show a => ([Char], a) -> IO() 
tests :: (Traversable t, Show a) => t ([Char], a) -> IO (t()) 

, die nur so lange, wie alle Tests haben die gleiche Art arbeitet für das zweite Argument. Ich nehme an, dass es den Typ irgendwie auf den tatsächlich angetroffenen Typ der Argumente spezialisiert hat, obwohl sie alle anzeigbar sind.

so dass sie auf den tatsächlichen Arten des zweiten Arguments variieren können, habe ich versucht, so etwas wie dieser (Pseudocode):

type ATest = (Show a) => (String, a) 

denn das würde nicht funktionieren, habe ich versucht:

{-# LANGUAGE RankNTypes #-} 
type ATest = forall a. (Show a) => (String, a) 

Welche kompiliert, aber immer noch bei jeder Variation im Wert Argument fehlschlägt.

Des Weiteren möchte ich abstrakt, um die Art der Tests aus der Schleife, die sie druckt, aber ich kann nicht dann verwenden, konvertieren von:

test :: Show a => ([Char], a) -> IO() 
to 
    test :: ATest -> IO() 

Die Grundidee einer polymorphen Typ zu definieren und zu verwenden, war für die Tests in der Definition der Testschleife. Also vielleicht eine Datenstruktur stattdessen;

data (Show a) => ATest = Test (String,a) 

aber das scheitert auch, obwohl es die richtige Idee gibt; Alle Tests haben eine gemeinsame Struktur mit einem zweiten Wert in der Typklasse anzeigen.

Was ist der richtige Ansatz dafür?

+0

Ich änderte den Titel in etwas, das das Thema ein wenig deutlicher widerspiegelt, aber ich bin nicht wirklich zufrieden mit meinem Vorschlag. Wenn jemand bessere Ideen hat, bitte bearbeiten Sie es. – duplode

Antwort

4

Lassen Sie uns mit Ihrem Kommentar zu den abgeleiteten Typen beginnen für test und tests:

test :: Show a => ([Char], a) -> IO() 
tests :: (Traversable t, Show a) => t ([Char], a) -> IO (t()) 

, die nur so lange, wie alle Tests haben die gleiche Art für das zweite Argument funktioniert. Ich gehe davon aus, dass es sich irgendwie um den Typ der tatsächlich aufgetretenen Art der Argumente handelt, obwohl sie alle darstellbar sind.

Das wird erwartet. Alle Elemente einer Liste (oder jeder anderen Traversable) müssen denselben Typ haben. Es ist nicht einmal eine Frage der "Spezialisierung": Sobald Typ Checker Sie versucht, z.zusammenbauen eine Liste aus einem Int und einem String, es sofort wirft seine Hände hoch:

GHCi> [3 :: Int, "foo"] 

<interactive>:125:12: error: 
    • Couldn't match expected type ‘Int’ with actual type ‘[Char]’ 
    • In the expression: "foo" 
     In the expression: [3 :: Int, "foo"] 
     In an equation for ‘it’: it = [3 :: Int, "foo"] 

Eine direkte Art und Weise zu umgehen, dass die Elemente eine Art zuweist, die die irrelevanten Differenzen zwischen den Werten ignoriert. Genau das haben Sie versucht, indem Sie forall ins Spiel gebracht haben - in Ihrem Fall haben Sie versucht zu sagen, dass das einzige, was am zweiten Element Ihrer Paare zählt, dass es eine Show Instanz für sie gibt:

Sie erwähnen, dass dieser Ansatz "immer noch bei jeder Variation im Wert Argument fehlschlägt". Ich konnte nicht diese spezielle Art des Versagens reproduzieren: In der Tat, ich konnte nicht einmal sagen, welche Art von einer Liste von ATest:

GHCi> :set -XRankNTypes 
GHCi> type ATest = forall a. (Show a) => (String, a) 
GHCi> -- There is no special meaning to :{ and :} 
GHCi> -- They are merely a GHCi trick for multi-line input. 
GHCi> :{ 
GHCi| glub :: [ATest] 
GHCi| glub = [("Sqrt(4)", sqrt(4.0)),("sumList:", sum [1,2,3,4])] 
GHCi| :} 

<interactive>:145:9: error: 
    • Illegal polymorphic type: ATest 
     GHC doesn't yet support impredicative polymorphism 
    • In the type signature: 
     glub :: [ATest] 

GHC ist bevorstehende über die Ablehnung: seit ATest nur eine Art Synonym ist, [ATest] wird auf [forall a. (Show a) => (String, a)] erweitert. Die forall innerhalb des Arguments für den List-Typ-Konstruktor erfordert ein Feature namens impredicative Polymorphism, die is not supported by GHC. Um zu vermeiden, dass in Laufen, brauchen wir einen richtigen Datentyp zu definieren, und nicht nur ein Synonym, das ist, was Sie in dem letzten Versuch zu tun versucht - außer, dass Sie müssen noch die forall wie vor:

GHCi> -- We need a different extension in this case. 
GHCi> :set -XExistentialQuantification 
GHCi> data ATest = forall a. Show a => ATest (String, a) 
GHCi> :{ 
GHCi| glub :: [ATest] 
GHCi| glub = [ATest ("Sqrt(4)", sqrt(4.0)),ATest ("sumList:", sum [1,2,3,4])] 
GHCi| :} 
GHCi> 

Dies, endlich, wie beabsichtigt funktioniert:

GHCi> :{ 
GHCi| -- I took the liberty of doing a few stylistic changes. 
GHCi| test :: ATest -> IO() 
GHCi| test (ATest (msg, val)) = print $ msg ++ " :: " ++ show val 
GHCi| 
GHCi| tests :: Foldable t => t ATest -> IO() 
GHCi| tests = mapM_ test 
GHCi| :} 
GHCi> tests glub 
"Sqrt(4) :: 2.0" 
"sumList: :: 10" 

auf einem allgemeinen Hinweis, ist es ratsam, nach Alternativen zu suchen, bevor sie sich festlegen existenzielle Quantifizierung auf diese Weise zu verwenden, da es oft mehr Probleme sein kann, als es ist wert (für einen Grund auf diese Diskussion, sieh zB die Frage und alle Antworten in How to convert my thoughts in OOP to Haskell?). In diesem Fall jedoch, in dem alles, was Sie tun möchten, ist, bequem eine Liste von Tests anzugeben, die ausgeführt werden sollen, scheint es sinnvoll, diese Strategie zu verwenden. In Testing QuickCheck properties against multiple types? sehen Sie ein Beispiel für etwas, das dem, was wir hier gemacht haben, sehr ähnlich ist, und zwar mit QuickCheck, einer vollständigen Testbibliothek für Haskell.

+0

Sehr schön, danke. Ich frage mich, warum man die existenzielle Quantifizierung (forall) in der Syntax dafür explizit machen muss und ein Pragma benötigt. Warum nicht einfach dieselbe Syntax wie für Typklassen, was mein ursprünglicher Versuch war? – guthrie

+0

@guthrie Gute Frage. Ich glaube, dass diese Funktion standardmäßig ausgeschaltet ist, nur weil sie einfach ist und auch Anfänger vor seltsamen Nebenwirkungen bei Tippfehlern schützt (zB 'data Foo = Bar | Baz a', wenn' Foo' normal sein sollte, nicht existentieller parametrischer Typ). Vgl. [die Bemerkungen auf dieser Haskell Prime-Seite] (https://prime.haskell.org/wiki/ExistentialQuantification).Nebenbei gesagt, "RankNTypes" (das die Verwendung von "forall" außer existenziellen Typen hinzufügt) ist standardmäßig auch ausgeschaltet, um Dinge einfach zu halten - es führt Situationen ein, in denen Typen ohne Signaturen nicht abgeleitet werden können. – duplode

3

In diesem Fall muss das Typsystem nicht gebogen werden. Sie brauchen nur

t1 = ("Sqrt(4)", show $ sqrt(4.0)) 
... 
t2 = ("sumList:", show $ sum [1,2,3,4]) 
... 
t3 = ("Description", show $ value) 
... 

Da das einzige, was man mit einem Ergebnis eines Tests tun können, ist es zu zeigen, könnte man genauso gut tun, dass sofort. Dank der Faulheit wird kein tatsächlicher Aufruf zum Erscheinen gemacht, bis das Ergebnis benötigt wird.

Wenn Sie eine Typklasse mit vielen Methoden haben, kann ein existentieller Typ Ihnen einen marginalen Vorteil verschaffen.

+1

Im Allgemeinen ist dies ein guter Rat. Ich folgte dieser Herangehensweise nicht meiner Antwort (abgesehen von den Anspielungen im letzten Absatz), weil ich annahm, dass das ultimative Ziel des OP das Schreiben von "Tests" war, was in diesem spezifischen Kontext nicht unangemessen ist. – duplode

+0

@ duplode, aber er kann "Tests" schreiben, weil "t1", "t2" und "t3" jetzt alle vom selben Typ sind. Das ist ein Punkt. –

+0

Ich sehe Ihren Punkt, und der Unterschied der Betonung in Ihrer Antwort macht es zu einem schönen Kontrapunkt zu meinem. Wenn das gesamte OP es wünscht, eine * ad hoc * Liste von Tests zu spezifizieren, die ausgeführt werden sollen, gibt es einen leichten Verlust an Bequemlichkeit, indem 't1', 't2', 't3' usw. getrennt definiert werden müssen . Während solche Dinge fast immer nicht ausreichen, um nach existentiellen Typen zu greifen, sind die Kosten im OP-spezifischen Anwendungsfall klein genug, um Sinn zu ergeben. – duplode

Verwandte Themen