4

假设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约束条件但可以是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

即调用者不需要知道所有的Yoos和诸如此类的东西。


编辑:澄清:我正在寻找Haskell程序员在这种情况下的方式。您将如何以惯用的方式处理它?

4

5 回答 5

8

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

  1. 代数数据类型

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

    优点:易于添加新操作
    缺点:添加新“类型”可能需要修改许多现有功能

  2. 支持的操作记录

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

    优点:易于添加新的“类型”
    缺点:添加新操作可能需要修改许多现有功能

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

类型类是为重载而设计的。使用它们来模仿 Haskell 中的面向对象编程通常是错误的。

于 2013-04-19T10:34:50.137 回答
5

类型类有点像 Java 风格的接口,但你使用它们的方式与使用接口的方式并不完全相同,因此这不是学习它们的好方法。

接口一种类型(因为 OO 语言有子类型,所以其他类型可以是接口的子类型,这就是你完成任何事情的方式)。Haskell 中的所有类型都是不相交的,所以类型类不是类型。它是一类型(实例声明是您声明集合成员的位置)。试着这样想它们。它使类型签名的正确读取更加自然(String -> a意味着“接受 aString并返回您想要的任何类型的值”,并且SomeTypeClass a => String -> a意味着“接受 aString并返回您想要的任何类型的值SomeTypeClass”)。

现在你不能按照你想要的方式做你想做的事,但我不确定为什么你需要按照你想要的方式去做。为什么不能paak只拥有 type String -> Yoo

您说您正在尝试执行以下操作:

let x = someGoo ....
in ham x ++ mustard x

如果someGoo ...paak "dummy string",那么x将是类型Yoo。ButYoo是 的成员Goo,因此您可以在其上调用和之Goo类的方法。如果您稍后更改为返回不同类型的值,那么编译器将告诉您使用任何特定功能的所有位置,并愉快地接受任何调用但仅使用该功能的位置。hammustardpaakGooYoopaakGoo

为什么你需要输入“一些未知类型,它是Goo”的成员?从根本上说,调用者paak 不会对 in 中的任何类型进行Goo操作,它们只会对paak实际返回的内容进行操作,即 a Yoo

您有一些对具体类型进行操作的函数,它们可以调用这些具体类型的函数以及来自具体类型所属的类型类的函数。或者您有对属于某个类型类的成员的任何类型进行操作的函数,在这种情况下,您可以调用的只是对类型类中的任何类型起作用的函数。

于 2013-04-19T10:44:41.350 回答
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}

这意味着,该函数实际上有一个完全独立的额外参数,即“字典参数”。这就是用于访问界面的内容。而且由于这确实是传递函数的参数,因此函数显然无法选择它。

要将字典函数中传递出去,你需要像我上面那样使用这样的恶作剧:你不直接返回多态结果,而是问调用者“你打算如何使用它?但是请注意,我可以'不要告诉你要得到什么具体类型......“也就是说,你要求他们给你自己一个多态函数,然后你可以在其中插入你选择的具体类型。

这样的功能可以这样使用:

myHamAndMustard = paak' arg (\myGoo -> ham myGoo ++ mustard myGoo )

这不是很好。同样,通常更好的方法是为所有可能的输出设置一个透明的非多态容器。很多时候,这仍然不是最佳的。您可能从太多的面向对象的角度来处理您的整个问题。

于 2013-04-19T09:39:39.473 回答
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 }

那么你可以统一调用hamall Goo

> let x = anyGoo Hoo
> let y = anyGoo Yoo
> ham x
"Hoo!"
> ham y
"Yoo!"

paak然后将返回 aAnyGoo而不是 a Goo

paak :: String -> AnyGoo
paak _ = anyGoo Yoo

但是那样的话,你(我)会再次传递某种类型,因此可以更好地返回hammar的建议。

于 2013-04-19T10:20:16.000 回答
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
于 2013-04-19T11:18:24.817 回答