我正在制作控制台程序,我将大量使用文本菜单。我写了一个函数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的新手。
谢谢。
答案 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演算中。