假设Goo
是我的类型类,通常声称它是与C ++,Java或C#等语言等效的接口:
class Goo goo where ham :: goo -> String
data Hoo = Hoo
instance Goo Hoo where ham _ = "Hoo!"
mustard _ = "Oh oh."
data Yoo = Yoo
instance Goo Yoo where ham _ = "Yoo!"
mustard _ = "Whew"
但我无法返回Goo
:
paak :: (Goo goo) => String -> goo
paak g = (Yoo)
-- Could not deduce (goo ~ Yoo)
-- from the context (Goo goo)
-- bound by the type signature for paak :: Goo goo => String -> goo
-- at frob.hs:13:1-14
-- `goo' is a rigid type variable bound by
-- the type signature for paak :: Goo goo => String -> goo
-- at frob.hs:13:1
-- In the expression: (Yoo)
-- In an equation for `paak': paak g = (Yoo)
我找到了这个启发性的陈述,它解释了原因:
类型
paak :: (Goo goo) => String -> goo
并不意味着该函数可能返回它想要的任何Goo
。这意味着该函数将返回用户想要的Goo
。
(从sepp2k的回答here音译)
但是,我怎么能返回或存储满足Goo
约束的东西,但可以是Hoo
,Yoo
,Moo
,Boo
或任何其他Goo
?
我是否只是在自己的编程背景中纠缠不清,需要完全不同的思考,比如采用类似C语言的界面:
data WhewIamAGoo = WhewIamAGoo {
ham' :: String
mustard' :: String
}
paak :: String -> WhewIamAGoo
paak g = let yoo = Yoo
in WhewIamAGoo { ham' = ham yoo
mustard' = mustard ham
}
但这似乎很尴尬。
在我的具体情况下,我想像这样使用Goo
:
let x = someGoo ....
in ham x ++ mustard x
即。调用者不应该知道所有Yoo
和诸如此类的内容。
编辑:澄清:我正在寻找Haskell程序员在这种情况下的方式。 如何以惯用的方式处理它?</ p>
答案 0 :(得分:8)
有两种解决这个问题的方法,我认为是惯用的Haskell:
代数数据类型
data Goo = Hoo | Yoo
ham Hoo = "Hoo!"
ham Yoo = "Yoo!"
mustard Hoo = "Oh oh."
mustard Yoo = "Whew"
亲:轻松添加新操作
Con:添加新的“类型”可能需要修改许多现有功能
支持的操作记录
data Goo = Goo { ham :: String, mustard :: String }
hoo = Goo { ham = "Hoo!", mustard = "Oh oh." }
yoo = Goo { ham = "Yoo!", mustard = "Whew" }
亲:轻松添加新的“类型”
Con:添加新操作可能需要修改许多现有功能
你当然可以混合搭配这些。一旦你习惯于考虑函数,数据和组合而不是接口,实现和继承,这些在大多数情况下都是足够好的。
类型类是为重载而设计的。使用它们来模拟Haskell中面向对象的编程通常是一个错误。
答案 1 :(得分:5)
Typeclasses有点像Java风格的界面,但你并没有像使用界面一样真正地使用它们,所以它不是学习它们的好方法。
接口是一种类型(因为OO语言具有子类型,因此其他类型可以是接口的子类型,这就是你完成任何事情的方式)。所有类型在Haskell中都是不相交的,因此类型类不类型。它是一个 set 类型(实例声明是你声明集合成员的地方)。试着用这种方式来思考它们。它使得签名类型的正确读取更加自然(String -> a
表示&#34;取String
并返回您想要的任何类型的值&#34;,SomeTypeClass a => String -> a
表示& #34;获取String
并返回您想要的任何类型的值SomeTypeClass
&#34;)。
现在你无法以你想要的方式做你想做的事,但我不确定你为什么需要按你想要的方式去做。为什么paak
只能拥有String -> Yoo
类型?
你说你正在尝试做类似的事情:
let x = someGoo ....
in ham x ++ mustard x
如果someGoo ...
为paak "dummy string"
,则x
的类型为Yoo
。但Yoo
是Goo
的成员,因此您可以在其上调用Goo
和ham
等mustard
方法。如果您稍后更改paak
以返回不同Goo
类型的值,则编译器将告诉您使用任何Yoo
特定功能的所有位置,并乐意接受未更改的任何位置调用paak
但后来只使用了Goo
功能。
为什么你需要输入它?#34;某些未知类型是Goo
&#34;的成员?从根本上说,paak
不的来电者对Goo
中的任何类型进行操作,他们只对paak
实际返回的内容进行操作,这是Yoo
1}}。
您有一些函数可以对具体类型进行操作,这些函数可以调用这些具体类型的函数以及来自类型类的函数,这些函数类型是具体类型的成员。或者你有一些函数可以在任何类型上运行,它是某个类型类的成员,在这种情况下,所有你可以调用的函数都适用于类型类中的任何类型。
答案 2 :(得分:2)
首先,通常没有必要!你的WhenIAmGoo
方法很好;因为Haskell是懒惰的,它没有任何真正的缺点,但往往更清晰。
但它仍有可能:
{-# LANGUAGE RankNTypes #-}
paak' :: String -> (forall goo . Goo goo => goo -> r) -> r
paak' g f = f Yoo
看起来很复杂?
要理解这个问题,您需要了解Haskell基于Hindley-Milner的类型系统的工作原理与C ++和Java做。在这些语言中,正如您所知,多态性基本上是一种有限的动态类型:如果您传递一个带有“接口类型”的对象,您实际上是在对象周围传递一个包装器,它知道接口方法是如何实现的在它。
在Haskell中,情况有所不同。明确编写的多态签名如下所示:
paak :: { forall goo . (Goo goo) } => {String -> goo}
这意味着,该函数实际上存在一个完全独立的额外参数,即“字典参数”。这就是用于访问界面的内容。由于这确实是一个将传递给函数的参数,因此该函数显然无法选择它。
要传递函数的字典 out ,你需要像我上面那样使用这些邪恶的技巧:你不直接返回多态结果,而是要求调用者“你好吗?打算使用它?但是请注意,我不知道你会得到什么样的具体类型......“也就是说,你需要它们给你自己一个多态函数,然后你可以插入你的具体类型选择。
这样的功能可以这样使用:
myHamAndMustard = paak' arg (\myGoo -> ham myGoo ++ mustard myGoo )
这不太好。同样,通常更好的方法是为所有可能的输出提供一个透明的非多态容器。很多时候,这仍然不是最佳的;你可能已经从太多的OO角度找到了整个问题。
答案 3 :(得分:2)
根据目前提供的信息,C风格的界面(==带功能的记录)似乎是要走的路。
但是,要使其更甜美,请添加智能构造函数并使AnyGoo
成为Goo
的实例:
data AnyGoo = AnyGoo {
ham' :: String
}
instance Goo AnyGoo where
ham = ham'
anyGoo :: (Goo goo) => goo -> AnyGoo
anyGoo goo = AnyGoo { ham' = ham goo }
然后,您可以统一调用所有ham
的{{1}}:
Goo
然后 > let x = anyGoo Hoo
> let y = anyGoo Yoo
> ham x
"Hoo!"
> ham y
"Yoo!"
将返回paak
而不是AnyGoo
:
Goo
然而,你(我)将再次传递某种类型,因此最好能够回复hammar的建议。
答案 4 :(得分:2)
我想支持回复@phresnel,但我想补充一些一般性的想法。
您应该了解的是,使用该签名paak :: (Goo goo) => String -> goo
,您试图通过类型系统控制您未来的评估。类型仅在编译时存在,如C#,C ++和其他OOP偏向语言。为了在运行时以不同的方式表示类型,这些语言使用虚函数表等。在Haskell中,你应该做同样的事情并将其包装在某些东西中。
data AnyGoo = forall a . (Goo a) => AnyGoo a
paak :: String -> AnyGoo
paak g = AnyGoo Yoo
在这种情况下,编译器(在ExistentialQuantification及其他东西的帮助下)为AnyGoo提供了多个构造函数(比如实现一个接口的类的多个构造函数),对于在Goo类型类中具有实例的任何类型都是开放的。
但在这种情况下,它足以使用数据值(如虚函数)。
data Goo = Goo { ham :: String }
-- ham :: Goo -> String
yoo = Goo { ham = "Yoo!" }
paak :: String -> AnyGoo
paak g = Goo yoo