2016-12-21 2 views
2

Ich mache ein Konsolenprogramm, in dem ich mit Textmenüs arbeiten werde. Ich schrieb eine Klasse Menu mit der Funktion choices, die eine Reihe von möglichen Menüoptionen und die Funktion parseChoice zurückgibt, die die vom Benutzer eingegebene Zeichenfolge in den Menüpunkt umwandeln.Haskell - mehrdeutige Klassenfunktion

data MainMenu = FirstItem | SecondItem 

class Menu a where 
    choices :: String -- ERROR HERE 
    parseChoice :: String -> Maybe a 

instance Menu MainMenu where 
    choices = "1) first choice\n2) second choice" 
    parseChoice "1" = Just FirstItem 
    parseChoice "2" = Just SecondItem 
    parseChoice _ = Nothing 

getMenuItem :: Menu a => IO a 
getMenuItem = do 
    putStrLn choices -- ERROR HERE 
    choice <- getLine 
    case parseChoice choice of 
    Just item -> return item 
    Nothing -> getMenuItem 

main :: IO() 
main = (getMenuItem :: IO MainMenu) >> return() 

Unfortunatelly, erhalte ich folgende Fehler

• Could not deduce (Menu a0) arising from a use of ‘choices’ 
    from the context: Menu a 
    bound by the type signature for: 
       getMenuItem :: Menu a => IO a 
    at [removed].hs:15:1-29 
    The type variable ‘a0’ is ambiguous 
    These potential instance exist: 
    instance Menu MainMenu 
     -- Defined at [removed].hs:9:10 
• In the first argument of ‘putStrLn’, namely ‘choices’ 
    In a stmt of a 'do' block: putStrLn choices 
    In the expression: 
    do { putStrLn choices; 
     choice <- getLine; 
     case parseChoice choice of { 
      Just item -> return item 
      Nothing -> getMenuItem } } 

Ich weiß, dass der Fehler tritt auf, weil Haskell wissen nicht, welche choices Funktion verwenden. Ich habe etwas wie putStrLn (choices :: Menu a) versucht, aber ohne Erfolg.

Fragen sind: Wo ist das Problem (und wie es zu beheben)? Sollte ich einen anderen Ansatz verwenden (was)?

Und bitte sei höflich, ich bin Haskell-Neuling.

Vielen Dank.

+2

Kurzantwort: Typklassenmethoden müssen den Typ entweder in einem Argument oder einem Rückgabetyp haben, so dass Ihr aktuelles Design mit 'choices :: String' nicht funktionieren kann. Der Grund dafür ist, dass der Compiler keine Möglichkeit hat, die Typklasse basierend darauf auszuwählen, wo sie verwendet wird. Sie müssen ein alternatives Design verwenden, aber ich habe keine Zeit, um jetzt eine zu finden :) – porges

Antwort

4

@porges ist richtig darüber, warum dies passiert, der Compiler hat einfach nicht genug Informationen zu wissen, welche Instanz der Typenklasse choices würde kommen. Stattdessen können Sie versuchen, es mit einem Phantom-Typ-Tagging:

data Choices a = Choices String 

class Menu a where 
    choices :: Choices a 
    parseChoices :: String -> Maybe a 

Dies allein wird nicht genug sein, müssen Sie den Typ mit Anmerkungen versehen, wo immer Sie verwenden choices:

putStrLn (choices :: Choices a) 

Diese ist aber nicht wirklich ideal. Eine Alternative ist es, die typeclass Ansatz zusammen und halten mit einem Grunddatentyp zu Graben:

data Menu a = Menu 
    { choices :: String 
    , parseChoices :: String -> Maybe a 
    } 

Dann können Sie

data MainMenu = FirstItem | SecondItem 

mainMenu :: Menu MainMenu 
mainMenu = Menu _choices _parseChoices where 
    _choices = "1) first choice\n2) second choice" 
    _parseChoices "1" = Just FirstItem 
    _parsechoices "2" = Just SecondItem 
    _parseChoices _ = Nothing 

tun Und schließlich

getMenuItem :: Menu a -> IO a 
getMenuItem [email protected](Menu choices parseChoices) = do 
    putStrLn choices 
    choice <- getLine 
    case parseChoice choice of 
     Just item -> return item 
     Nothing -> getMenuItem menu 

main :: IO() 
main = (getMenuItem mainMenu) >> return() 
3

Das Problem ist, dass die Leitung putStrLn choices ist von Natur aus mehrdeutig. Wenn mehrere Instanzen der Klasse Menu verfügbar sind, könnte das bedeuten, dass Sie eine von ihnen drucken. Sie könnten beabsichtigen, die von der 10 Konstanz bereitgestellte Instanz zu verwenden, aber ein anderer Programmierer könnte das vermeiden wollen und die Menu MainMenu Instanz wählen, ohne a zu berücksichtigen.

Eine Option wäre, Typklassen zu vermeiden. Dies ist wahrscheinlich der vernünftigere, einfachere und effektivste Weg. So stellen Sie Menu eine Art wie

data Menu = Menu { choices :: String , ... } 

und einen Wert dieses Typs um passieren, manuell.

Angenommen, wir mit dem typeclass aus irgendeinem Grund bleiben wollen, können wir die Täterleitung durch Änderung der Art der choices disambiguate wie folgt:

{-# LANGUAGE ScopedTypeVariables #-} 
import Data.Proxy 

class Menu a where 
    choices :: proxy a -> String 
    ... 

getMenuItem :: forall a. Menu a => IO a 
getMenuItem = do 
    putStrLn (choices (Proxy :: Proxy a)) 
    ... 

Der zusätzliche Proxy-Argument hat einen Dummy-Wert. Zur Laufzeit enthält es keine Informationen, aber zur Kompilierzeit ermöglicht es dem Compiler, zu disambiguieren.

Alternativ mit einigen anderen neueren GHC Erweiterungen kann man einen Code benutzen, die mit dem Original näher

{-# LANGUAGE ScopedTypeVariables, AllowAmbiguousTypes, TypeApplications #-} 

class Menu a where 
    choices :: String 
    ... 

getMenuItem :: forall a. Menu a => IO a 
getMenuItem = do 
    putStrLn (choices @ a) 
    ... 

Dieses ganz neuen Stil, aber die Chancen sind, dass dies eine Menge in der Zukunft verwendet werden . Dies liegt daran, dass es einfacher ist, Proxies weiterzugeben. Selbst Typtheoretiker sollten explizite Typargumente zu schätzen wissen, die häufig in vielen typisierten Lambda-Kalkülen gefunden werden.