[英]Polymorphic return type, interfaces, callbacks?
假设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的答案音译到这里 )
但是,我怎么能返回或存储满足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
这样使用Goo
:
let x = someGoo ....
in ham x ++ mustard x
即呼叫者不应该知道所有的Yoo
和诸如此类的东西。
编辑:澄清:我正在寻找Haskell程序员在这种情况下的方式。 你会如何以惯用的方式处理它?
有两种解决这个问题的方法我认为是惯用的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中面向对象的编程通常是一个错误。
类型类似于Java风格的接口,但是你并没有像使用接口那样使用它们,所以它不是学习它们的好方法。
接口是一种类型(因为OO语言具有子类型,因此其他类型可以是接口的子类型,这是您完成任何操作的方式)。 所有类型在Haskell中都是不相交的,因此类型类不是类型。 它是一组类型(实例声明是您声明集合成员的位置)。 试着用这种方式来思考它们。 它使得签名类型的正确读取更加自然( String -> a
表示“接受一个String
并返回您想要的任何类型的值”,而SomeTypeClass a => String -> a
表示“接受一个String
并返回一个值”您想要的任何类型的SomeTypeClass
“)的成员。
现在你无法以你想要的方式做你想做的事,但我不确定你为什么需要按你想要的方式去做。 为什么paak
只能有类型String -> Yoo
?
你说你正在尝试做类似的事情:
let x = someGoo ....
in ham x ++ mustard x
如果someGoo ...
是paak "dummy string"
,那么x
将是Yoo
类型。 但是Yoo
是Goo
的成员,所以你可以在上面调用像ham
和mustard
这样的Goo
方法。 如果您稍后更改paak
以返回不同Goo
类型的值,那么编译器将告诉您使用任何Yoo
特定功能的所有位置,并且愉快地接受未更改的任何调用paak
但仅使用Goo
功能的位置。
为什么你需要输入“某些未知类型,这是Goo
的成员”? 从根本上说, paak
调用者不会在Goo
以任何类型操作,他们只能根据paak
实际返回的内容进行操作,这是一个Yoo
。
您有一些对具体类型进行操作的函数,它们可以调用这些具体类型上的函数以及来自类型类的函数,这些函数类型是具体类型的成员。 或者你有一些函数可以在任何类型上运行,这些类型是某个类型类的成员,在这种情况下你可以调用的函数是在类类中的任何类型上工作的函数。
首先,通常没有必要! 你的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}
这意味着,该函数实际上存在一个完全独立的额外参数,即“字典参数”。 这就是用于访问界面的内容。 因为这确实是传递给函数的参数,所以函数显然无法选择它。
要将字典从函数中传出 ,你需要像我上面那样使用这些邪恶的技巧:你不直接返回多态结果,而是要求调用者“你将如何使用它?但是请注意,我可以不要告诉你将会得到什么样的具体类型......“即,你需要它们给你自己一个多态函数,然后你可以插入你选择的具体类型。
这样的功能可以这样使用:
myHamAndMustard = paak' arg (\myGoo -> ham myGoo ++ mustard myGoo )
这不是很好。 同样,通常更好的方法是为所有可能的输出提供一个透明的非多态容器。 很多时候,这仍然不是最佳的; 你可能已经从太多的OO角度接近了整个问题。
根据目前提供的信息,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 }
那么你可以统一称为所有Goo
ham
:
> let x = anyGoo Hoo
> let y = anyGoo Yoo
> ham x
"Hoo!"
> ham y
"Yoo!"
然后paak
将返回AnyGoo
而不是Goo
:
paak :: String -> AnyGoo
paak _ = anyGoo Yoo
但是,你(我)将再次传递某种类型,因此可以更好地回归hammar的建议。
我想支持回答@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
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.