Haskell - 模糊的类功能

时间:2016-12-21 22:13:43

标签: haskell

我正在制作控制台程序,我将大量使用文本菜单。我写了一个函数Menu的类choices,它返回可能的菜单选项字符串和函数parseChoice,它将用户输入的字符串转换为菜单项。

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 ()

不幸的是,我收到了以下错误

• 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 } }

我知道发生错误是因为Haskell不知道哪个choices函数使用了。我试过像putStrLn (choices :: Menu a)这样的东西但没有成功。

问题是:问题出在哪里(以及如何解决)?我应该使用不同的方法吗?

请礼貌,我是Haskell的新手。

谢谢。

2 个答案:

答案 0 :(得分:5)

@porges对于为什么会发生这种情况是正确的,编译器根本没有足够的信息来知道类型类choices的哪个实例将来自哪个。相反,您可以尝试使用幻像类型对其进行标记:

data Choices a = Choices String

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

单凭这一点已经足够了,您需要在使用choices的地方注释类型:

putStrLn (choices :: Choices a)
但是,这并不是很理想。另一种方法是完全放弃类型类方法并坚持使用基本数据类型:

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

然后你可以做

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

最后

getMenuItem :: Menu a -> IO a
getMenuItem menu@(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 ()

答案 1 :(得分:4)

问题是,行putStrLn choices本质上是模棱两可的。当类Menu的多个实例可用时,它可能意味着打印它们中的任何一个。在您看来,您可能打算使用Menu a => constaint提供的实例,但另一位程序员可能希望避免这种情况并选择Menu MainMenu实例,而忽略a

一种选择是避免类型类。这可能是更健全,更简单,最有效的方法。只需将Menu设为类似

的类型即可
data Menu = Menu { choices :: String , ... }

并手动传递该类型的值。

假设我们想出于某种原因坚持使用类型类,我们可以通过更改choices的类型消除罪魁祸首,如下所示:

{-# 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))
  ...

附加代理参数具有虚拟值。在运行时它不携带任何信息,但在编译时它允许编译器消除歧义。

或者,对于其他一些较新的GHC扩展,可以使用一些更接近原始代码的代码

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

class Menu a where
   choices :: String
   ...

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

这是一种非常新的风格,但很有可能在未来会有很多用处。这是因为它比传递代理更简单。甚至类型理论家也应该理解显式类型参数,这些参数通常存在于许多类型的lambda演算中。