2012-07-29 5 views
9

Betrachten Sie das folgende Codebeispiel:Haskell Sub-Typklasse erfordert UndecidableInstances?

{-# LANGUAGE FlexibleInstances #-} 
{-# LANGUAGE UndecidableInstances #-} -- Is there a way to avoid this? 

-- A generic class with a generic function. 
class Foo a where 
    foo :: a -> a 

-- A specific class with specific functions. 
class Bar a where 
    bar :: a -> a 
    baz :: a -> a 

-- Given the specific class functions, we can implement the generic class function. 
instance Bar a => Foo a where 
    foo = bar . baz 

-- So if a type belongs to the specific class... 
instance Bar String where 
    bar = id 
    baz = id 

-- We can invoke the generic function on it. 
main :: IO() 
main = 
    putStrLn (foo "bar") 

(Mein eigentlicher Code ist viel aufwendiger, das ist ein minimal gekocht-down Fall das Muster zu demonstrieren.)

Es ist mir nicht klar, warum UndecidableInstances werden hier benötigt - der Typparameter a erscheint einmal in beiden Seiten der Bar a => Foo a, so erwartete ich Dinge "nur" zu arbeiten. Ich vermisse hier offensichtlich etwas. Aber gibt es einen Weg, dies zu tun, ohne UndecidableInstances zu verwenden?

+2

Durch die Deklaration der 'Instanz Bar a => Foo a' Sie eine Instanz eingeführt haben, für jeden' ​​a' - Kontext 'Bar a' nicht in Instanz Auswahl verwendet - obwohl, wenn Sie überlappende Instanzen haben GHC die meisten finden spezifisch für jedes Modul. –

+0

Das ist ... Zähler intuitiv :-) Gibt es einen Weg um es herum? Ich nehme an, dass "das Finden der spezifischsten Instanz" das ist, was "UndecidableInstances" versuchen wird, aber in meinem Code versagt es kläglich. –

+0

Verwandte: http://stackoverflow.com/questions/3213490 – sdcvvc

Antwort

8

Es gibt ein paar Ansätze, die Sie ergreifen können; Ich glaube nicht, dass Sie genügend Kontext zur Verfügung gestellt haben, um zu bestimmen, welches das am besten geeignete wäre. Wenn Sie GHC-7.4 verwenden, sollten Sie die Erweiterung DefaultSignatures ausprobieren.

{-# LANGUAGE FlexibleInstances #-} 
{-# LANGUAGE DefaultSignatures #-} 

-- A generic class with a generic function. 
class Foo a where 
    foo :: a -> a 
    default foo :: Bar a => a -> a 
    foo = bar . baz 

-- A specific class with specific functions. 
class Bar a where 
    bar :: a -> a 
    baz :: a -> a 

instance Bar String where 
    bar = id 
    baz = id 

instance Foo String 

main :: IO() 
main = 
    putStrLn (foo "bar") 

Sie müssen noch erklären, dass ein Typ eine Instanz von Foo ist, aber Sie brauchen nicht auf die Methode Erklärung zu wiederholen, da die Standardimplementierung verwendet wird.

Ein anderer ziemlich leichter Ansatz ist die Verwendung eines neuen Typs. Wenn Sie Funktionen haben, die eine Foo Instanz benötigen, können Sie eine Bar Instanz in den newtype umbrechen.

newtype FooBar a = FooBar { unFooBar :: a } 

instance Bar a => Foo (FooBar a) where 
    foo = FooBar . bar . baz . unFooBar 

-- imported from a library or something... 
needsFoo :: Foo a => a -> b 

myFunc = needsFoo (FooBar someBar) 

Alternativ können Sie in der Lage sein, mit zu erhalten, indem foo mit einer normalen Funktion zu ersetzen, oder machen eine spezielle Version für Bar Instanzen:

-- if every `Foo` is also a `Bar`, you can just do this. No need for `Foo` at all! 
foo :: Bar a => a -> a 
foo = bar . baz 

-- if most `Foo`s aren't `Bar`s, you may be able to use this function when you have a `Bar` 
fooBar :: Bar a => a -> a 
foo = bar . baz 

Dies sind wahrscheinlich die besten Lösungen, wenn sie arbeiten für deine Situation.

Eine andere Möglichkeit besteht darin, jede Foo Instanz manuell zu deklarieren. Obwohl es viele verschiedene vorstellbare Instanzen gibt, ist es ziemlich häufig für Codebasen, nur eine Handvoll Instanzen zu haben, die tatsächlich verwendet werden. Wenn das hier wahr ist, ist es wahrscheinlich weniger Arbeit, nur die 3 oder 4 Instanzen zu schreiben, die Sie brauchen, anstatt zu versuchen, eine allgemeinere Lösung zu implementieren.

Als allerletzte Mittel, können Sie so etwas wie Ihre Original-Code verwenden, aber Sie müssen auch OverlappingInstances, damit es funktioniert (wenn Sie OverlappingInstances nicht brauchen, dann brauchen Sie keine Foo Klasse). Mit dieser Erweiterung kann GHC die "spezifischste Instanz" auswählen, wenn mehrere Übereinstimmungen vorhanden sind. Dies wird mehr oder weniger funktionieren, obwohl Sie möglicherweise nicht bekommen, was Sie erwarten.

class Foo a where 
    foo :: a -> a 

class Bar a where 
    bar :: a -> a 
    baz :: a -> a 

instance Bar String where 
    bar = id 
    baz = id 

instance Bar a => Foo a where 
    foo = bar . baz 

instance Foo [a] where 
    foo _ = [] 

main :: IO() 
main = 
    print (foo "foo") 

Jetzt main druckt eine leere Zeichenfolge. Es gibt zwei Foo Instanzen für a und [a]. Letzteres ist spezifischer, so dass es für foo "foo" gewählt wird, da ein String den Typ [Char] hat, obwohl Sie wahrscheinlich ersteres wollten. So, jetzt würden Sie auch

instance Foo String where 
    foo = bar . baz 

bei denen schreiben müssen zeigen Sie auch die ganz Bar a => Foo a Instanz auslassen kann.

+0

Wow, danke für die ausführliche Antwort! DefaultSignatures funktioniert nicht für mich, da sie den Standard in der generischen Klasse deklarieren müssen und ich möchte, dass Benutzer neue spezifische Klassen frei definieren können. Der Newtype-Ansatz könnte für mich funktionieren, ich muss ihn jedoch an meinen komplexeren Fall anpassen. Die Verwendung von Funktionen würde für mich nicht funktionieren ... OverlappingInstances könnten ebenfalls funktionieren, da in meinem Fall kaum Überschneidungen der von Ihnen beschriebenen Art auftreten. Wenn es funktioniert, wäre es am besten, weil es am wenigsten aufdringlich ist. Danke noch einmal! –

+0

@ OrenBen-Kiki - Ich kann nicht von irgendeiner Weise vorstellen, dass ein Standardsignatur weitere Verwendung von 'foo' beschränken würde, ob es Fälle, Unterklassen umfasst, oder die 'foo' Methode in ansonsten nicht verwandten Funktionen. Es ist sehr unklar, was genau Sie hier zu modellieren versuchen, aber ich vermute, dass Sie versuchen, eine Hierarchie im OOP-Stil mit Typklassen einzurichten. Wenn ja, würde ich vorschlagen, dass Sie eine weitere Frage zu SO stellen, um nach Vorschlägen zur Modellierung Ihres Problems in Haskell zu fragen. –

+0

Auch ein Wort der Warnung. Wenn Sie den 'OverlappingInstances' Weg zu gehen entscheiden, an einem gewissen Punkt GHC beschweren kann, die es braucht' IncoherentInstances' aktiviert. Tu es nicht, es wird nicht helfen. –

0

Zusätzlich zu oben zu beantworten. Politik verwendet in Data.Traversable Modul von base Bibliothek ist attraktiv.Kurz gesagt, das Generieren einer generischen Instanz in der Bibliothek zwingt den Endbenutzer dazu, Ihre Entscheidung zu akzeptieren, und dies ist nicht immer das Beste, was zu tun ist. Data.Traversable enthält Funktionen wie foldMapDefault, die Standardimplementierung gibt, aber Entscheidung der konkreten Umsetzung ist noch bis zum Anwender.