多态返回类型,接口,回调?

时间:2013-04-19 09:19:59

标签: haskell

假设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约束的东西,但可以是HooYooMooBoo或任何其他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>

5 个答案:

答案 0 :(得分:8)

有两种解决这个问题的方法,我认为是惯用的Haskell:

  1. 代数数据类型

    data Goo = Hoo | Yoo
    
    ham Hoo = "Hoo!"
    ham Yoo = "Yoo!"
    
    mustard Hoo = "Oh oh."
    mustard Yoo = "Whew"
    

    亲:轻松添加新操作
    Con:添加新的“类型”可能需要修改许多现有功能

  2. 支持的操作记录

    data Goo = Goo { ham :: String, mustard :: String }
    
    hoo = Goo { ham = "Hoo!", mustard = "Oh oh." }
    yoo = Goo { ham = "Yoo!", mustard = "Whew" }
    

    亲:轻松添加新的“类型”
     Con:添加新操作可能需要修改许多现有功能

  3. 你当然可以混合搭配这些。一旦你习惯于考虑函数,数据和组合而不是接口,实现和继承,这些在大多数情况下都是足够好的。

    类型类是为重载而设计的。使用它们来模拟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。但YooGoo的成员,因此您可以在其上调用Goohammustard方法。如果您稍后更改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