简体   繁体   English

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

[英]Polymorphic return type, interfaces, callbacks?

Let's say Goo is my type class, which is often claimed to be the interface equivalent in languages like C++, Java or C#: 假设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"

But I cannot return a Goo : 但我不能回复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)

I found this enlightening statement, which explains why: 我找到了这个启发性的陈述,它解释了为什么:

The type paak :: (Goo goo) => String -> goo does not mean that the function might return any Goo it wants. 类型paak :: (Goo goo) => String -> goo并不意味着该函数可能返回它想要的任何Goo It means that the function will return whichever Goo the user wants. 这意味着该函数将返回用户想要的Goo

(transliterated from sepp2k's answer here ) (从sepp2k的答案音译到这里

But then, how could I return or store something that satisfies the Goo constraints, but can be Hoo , Yoo , Moo , Boo or any other Goo ? 但是,我怎么能返回或存储满足Goo约束的东西,但可以是HooYooMooBoo或任何其他Goo

Am I just entangled too much in own programming background, and need to think completely different, like resorting to C-like interfaces: 我只是在自己的编程背景中纠缠不清,需要完全不同的思考,比如求助于类似C的界面:

data WhewIamAGoo = WhewIamAGoo {
    ham' :: String
    mustard' :: String
}

paak :: String -> WhewIamAGoo
paak g = let yoo = Yoo 
         in WhewIamAGoo { ham' = ham yoo
                          mustard' = mustard ham
                        }

But that seems awkward. 但这似乎很尴尬。

In my specific case, I would like to use Goo like this: 在我的具体情况下,我Goo这样使用Goo

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

Ie the caller should not need to know about all the Yoo s and whatnot. 即呼叫者不应该知道所有的Yoo和诸如此类的东西。


edit: To clarify: I am looking for the way a Haskell programmer would go in such situation. 编辑:澄清:我正在寻找Haskell程序员在这种情况下的方式。 How would you handle it in an idiomatic way? 如何以惯用的方式处理它?

There are two ways of solving this problem that I consider idiomatic Haskell: 有两种解决这个问题的方法我认为是惯用的Haskell:

  1. Algebraic data type 代数数据类型

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

    Pro: easy to add new operations 亲:轻松添加新操作
    Con: adding a new "type" potentially requires modifying many existing functions Con:添加新的“类型”可能需要修改许多现有功能

  2. Record of supported operations 记录支持的操作

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

    Pro: easy to add new "types" 亲:轻松添加新的“类型”
    Con: adding a new operation potentially requires modifying many existing functions Con:添加新操作可能需要修改许多现有功能

You can of course mix and match these. 你当然可以混合搭配这些。 Once you get used to thinking about functions, data and composition rather than interfaces, implementations and inheritance, these are good enough in a majority of cases. 一旦你习惯于考虑函数,数据和组合而不是接口,实现和继承,这些在大多数情况下都是足够好的。

Type classes are designed for overloading. 类型类是为重载而设计的。 Using them to mimic object-oriented programming in Haskell is usually a mistake. 使用它们来模拟Haskell中面向对象的编程通常是一个错误。

Typeclasses are a bit like Java-style interfaces, but you don't really use them precisely the same way you use interfaces, so it's not a great way to learn them. 类型类似于Java风格的接口,但是你并没有像使用接口那样使用它们,所以它不是学习它们的好方法。

An interface is a type (because OO languages have subtypes, so other types can be subtypes of the interface, which is how you get anything done). 接口一种类型(因为OO语言具有子类型,因此其他类型可以是接口的子类型,这是您完成任何操作的方式)。 All types are disjoint in Haskell, so a type class is not a type. 所有类型在Haskell中都是不相交的,因此类型类不是类型。 It's a set of types (the instance declarations are where you declare what the members of the set are). 它是一类型(实例声明是您声明集合成员的位置)。 Try to think of them this way. 试着用这种方式来思考它们。 It makes the correct reading of type signatures much more natural ( String -> a means "takes a String and returns a value of any type you want", and SomeTypeClass a => String -> a means "takes a String and returns a value of any type you want that is a member of SomeTypeClass "). 它使得签名类型的正确读取更加自然( String -> a表示“接受一个String并返回您想要的任何类型的值”,而SomeTypeClass a => String -> a表示“接受一个String并返回一个值”您想要的任何类型的SomeTypeClass “)的成员。

Now you can't do what you want the way you want it, but I'm not sure why you need to do it the way you want it. 现在你无法以你想要的方式做你想做的事,但我不确定你为什么需要按你想要的方式去做。 Why can't paak just have the type String -> Yoo ? 为什么paak只能有类型String -> Yoo

You say you're trying to do something like: 你说你正在尝试做类似的事情:

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

If someGoo ... is paak "dummy string" , then x will be of type Yoo . 如果someGoo ...paak "dummy string" ,那么x将是Yoo类型。 But Yoo is a member of Goo , so you can call Goo methods like ham and mustard on it. 但是YooGoo的成员,所以你可以在上面调用像hammustard这样的Goo方法。 If you later change paak to return a value in a different Goo type, then the compiler will tell you all the places that used any Yoo -specific functionality, and happily accept unchanged any places that called paak but then only used the Goo functionality. 如果您稍后更改paak以返回不同Goo类型的值,那么编译器将告诉您使用任何Yoo特定功能的所有位置,并且愉快地接受未更改的任何调用paak但仅使用Goo功能的位置。

Why do you need it to be typed "some unknown type which is a member of Goo "? 为什么你需要输入“某些未知类型,这是Goo的成员”? Fundamentally, callers of paak don't operate on any type in Goo , they only operate on what paak actually returns, which is a Yoo . 从根本上说, paak调用者不会Goo以任何类型操作,他们只能根据paak实际返回的内容进行操作,这是一个Yoo

You have some functions that operate on concrete types, which can call functions on those concrete types as well as functions that come from type classes of which the concrete type is a member. 您有一些对具体类型进行操作的函数,它们可以调用这些具体类型上的函数以及来自类型类的函数,这些函数类型是具体类型的成员。 Or you have functions which operate on any type which is a member of some type class, in which case all you can call are functions that work on any type in the type class. 或者你有一些函数可以在任何类型上运行,这些类型是某个类型类的成员,在这种情况下你可以调用的函数是在类类中的任何类型上工作的函数。

First of all, it's generally not necessary! 首先,通常没有必要! Your WhenIAmGoo approach is just fine; 你的WhenIAmGoo方法很好; since Haskell is lazy it doesn't have any real drawbacks but is often much clearer. 因为Haskell是懒惰的,它没有任何真正的缺点,但往往更清晰。

But it's still possible: 但它仍然可能:

{-# LANGUAGE RankNTypes              #-}

paak' :: String -> (forall goo . Goo goo => goo -> r) -> r
paak' g f = f Yoo

Looks complicated? 看起来很复杂

To understand the issue you need to know how Haskell's Hindley-Milner -based type system works quite fundamentally different from what eg C++ and Java do. 要理解这个问题,你需要知道Haskell的基于Hindley-Milner的类型系统与C ++和Java的工作方式有着根本的不同。 In these languages, as you seem to know, polymorphism is basically a kind of limited dynamic typing: if you pass an object with an "interface type", you actually rather pass a wrapper around an object, which knows how the interface methods are implemented in it. 在这些语言中,正如您所知,多态性基本上是一种有限的动态类型:如果您传递一个带有“接口类型”的对象,您实际上是在对象周围传递一个包装器,它知道接口方法是如何实现的在里面。

In Haskell, it's different. 在Haskell中,它是不同的。 A polymorphic signature, explicitly written, looks like this: 明确编写的多态签名如下所示:

paak :: { forall goo . (Goo goo) } => {String -> goo}

which means, there is actually a completely seperate extra argument to the function, the "dictionary argument". 这意味着,该函数实际上存在一个完全独立的额外参数,即“字典参数”。 That's what's used to access the interface. 这就是用于访问界面的内容。 And since this is indeed an argument passed to the function, the function obviously doesn't get to choose it. 因为这确实是传递函数的参数,所以函数显然无法选择它。

To pass the dictionary out of the function, you need to employ such evil tricks as I did above: you don't directly return a polymorphic result, but rather ask the caller "how are you going to use it? But mind, I can't tell what concrete type you're going to get..." Ie, you require them to give you themselves a polymorphic function, into which you can then insert the concrete type of your choice. 要将字典函数中传出 ,你需要像我上面那样使用这些邪恶的技巧:你不直接返回多态结果,而是要求调用者“你将如何使用它?但是请注意,我可以不要告诉你将会得到什么样的具体类型......“即,你需要它们给你自己一个多态函数,然后你可以插入你选择的具体类型。

Such a function can be used this way: 这样的功能可以这样使用:

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

which isn't exactly nice. 这不是很好。 Again, the usually better way is to have just have a transparent non-polymorphic container for all the possible outputs. 同样,通常更好的方法是为所有可能的输出提供一个透明的非多态容器。 Very often, that's still not optimal; 很多时候,这仍然不是最佳的; you probably have approached you entire problem from too much of an OO angle. 你可能已经从太多的OO角度接近了整个问题。

Based on the information provided so far, the C-style interface (== record with functions) seems to be the way to go. 根据目前提供的信息,C风格的界面(==带功能的记录)似乎是要走的路。

However, to make it sweeter to use, add a smart constructor and make AnyGoo an instance of Goo : 但是,为了使其更甜美,添加一个智能构造函数并使AnyGoo成为Goo一个实例:

data AnyGoo = AnyGoo {
    ham' :: String 
}
instance Goo AnyGoo where
    ham = ham' 


anyGoo :: (Goo goo) => goo -> AnyGoo 
anyGoo goo = AnyGoo { ham' = ham goo }

then you can uniformly call ham for all Goo : 那么你可以统一称为所有Goo ham

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

paak would then return an AnyGoo instead of a Goo : 然后paak将返回AnyGoo而不是Goo

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

But then, you (I) would be passing a certain type around again, thus could better return hammar's suggestion. 但是,你(我)将再次传递某种类型,因此可以更好地回归hammar的建议。

I'd like to support answer @phresnel but I'd like to add some general thoughts. 我想支持回答@phresnel,但我想补充一些一般性的想法。

What you should understand is that with that signature paak :: (Goo goo) => String -> goo you are trying to control your future evaluation with type system. 你应该理解的是,使用那个签名paak :: (Goo goo) => String -> goo你试图用类型系统来控制你未来的评估。 Types exists only in compile time like in C#, C++ and other OOP biased languages. 类型仅在编译时存在,如C#,C ++和其他OOP偏向语言。 To get types represented differently in run-time such languages uses tables of virtual functions etc. In Haskell you should do the same thing and wrap it in something. 为了在运行时以不同的方式表示类型,这些语言使用虚函数表等。在Haskell中,你应该做同样的事情并将其包装在某些东西中。

data AnyGoo = forall a . (Goo a) => AnyGoo a

paak :: String -> AnyGoo
paak g = AnyGoo Yoo

In this case compiler (with help of ExistentialQuantification and other stuff) have multiple constructors (like with multiple constructors of classes that implements one interface) for AnyGoo which is open for any type that have instance within Goo typeclass. 在这种情况下,编译器(在ExistentialQuantification和其他东西的帮助下)具有多个构造函数(比如具有实现一个接口的类的多个构造函数),用于AnyGoo,对于在Goo类型类中具有实例的任何类型都是开放的。

But in this case its enough to use data value (like virtual functions). 但在这种情况下,它足以使用数据值(如虚函数)。

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM