Haskell中的存在主义类型和其他语言中的泛型



我试图使用Haskell/Existentially quantified types一文来掌握Haskell中存在主义类型的概念。乍一看,这个概念似乎很清楚,有点类似于面向对象语言中的泛型。主要的例子是所谓的"异构列表",定义如下:

data ShowBox = forall s. Show s => SB s

heteroList :: [ShowBox]
heteroList = [SB (), SB 5, SB True]
instance Show ShowBox where
show (SB s) = show s

f :: [ShowBox] -> IO ()
f xs = mapM_ print xs
main = f heteroList

我对"异构列表"有不同的概念,比如Scala中的Shapeless。但在这里,它只是一个包含在存在类型中的项列表,只添加了一个类型约束。其元素的确切类型并未体现在其类型签名中,我们唯一知道的是它们都符合类型约束。

在面向对象的语言中,编写这样的东西似乎是很自然的(例如在Java中)。这是一个无处不在的用例,我不需要创建包装器类型来处理所有实现某个接口的对象列表。animals列表有一个泛型类型List<Vocal>,所以我可以假设它的元素都符合这个Vocal接口:

interface Vocal {
void voice();
}
class Cat implements Vocal {
public void voice() {
System.out.println("meow");
}
}
class Dog implements Vocal {
public void voice() {
System.out.println("bark");
}
}
var animals = Arrays.asList(new Cat(), new Dog());
animals.forEach(Vocal::voice);

我注意到存在类型只能作为语言扩展使用,并且在大多数"基本"Haskell书籍或教程中都没有描述它们,所以我的建议是这是一个相当高级的语言功能。

我的问题是,为什么?在 Haskell 中,在具有泛型的语言中似乎是基本的东西(构造和使用其类型实现某些接口并多态访问它们的对象列表)需要语言扩展、自定义语法和创建额外的包装器类型?是否没有办法在不使用存在类型的情况下实现这样的事情,或者只是没有基本的用例?

或者也许我只是混淆了概念,存在主义类型和泛型意味着完全不同的东西。请帮助我理解它。

是的,存在类型和泛型意味着不同的东西。存在类型可以类似于面向对象语言中的接口。当然,您可以在列表中放置一个,但是使用接口不需要列表或任何其他泛型类型。拥有类型Vocal的变量来演示其用法就足够了。

它在Haskell中没有广泛使用,因为大多数时候它并不是真正需要的。

nonHeteroList :: [IO ()]
nonHeteroList = [print (), print 5, print True]

在没有任何语言扩展的情况下做同样的事情。

存在类型(或面向对象语言中的接口)只不过是一段带有捆绑方法字典的数据。如果您的字典中只有一个方法,只需使用一个函数。如果您有多个元组,则可以使用元组或这些元组的记录。所以如果你有类似的东西

interface Shape {
void Draw();
double Area();
}

你可以用Haskell来表达它,例如,

type Shape = (IO (), Double)

并说

circle center radius = (drawCircle center radius, pi * radius * radius)
rectangle topLeft bottomRight = (drawRectangle topLeft bottomRight, 
abs $ (topLeft.x-bottomRight.x) * (topLeft.y-bottomRight.y))
shapes = [circle (P 2.0 3.5) 4.2, rectangle (P 3.3 7.2) (P -2.0 3.1)]

虽然你可以用类型类、实例和存在来表达完全相同的东西

class Shape a where
draw :: a -> IO ()
area :: a -> Double
data ShapeBox = forall s. Shape s => SB s
instance Shape ShapeBox where
draw (SB a) = draw a
area (SB a) = area a
data Circle = Circle Point Double
instance Shape Circle where
draw (Circle c r) = drawCircle c r
area (Circle _ r) = pi * r * r
data Rectangle = Rectangle Point Point
instance Shape Rectangle where
draw (Rectangle tl br) = drawRectangle tl br
area (Rectangle tl br) = abs $ (tl.x - br.x) * (tl.y - br.y)
shapes = [Circle (P 2.0 3.5) 4.2, Rectangle (P 3.3 7.2) (P -2.0 3.1)]

你有它,N 倍长。

这没有基本的用例吗?

有点,是的。在Java中,你别无选择,只能有开放类,Haskell有ADT,你通常会用在这类用例中使用。在您的示例中,Haskell可以用以下两种方式之一表示它:

data Cat = Cat
data Dog = Dog
class Animal a where
voice :: a -> String
instance Animal Cat where
voice Cat = "meow"
instance Animal Dog where
voice Dog = "woof"

data Animal = Cat | Dog
voice Cat = "meow"
voice Dog = "woof"

如果你需要一些可扩展的东西,你会使用前者,但如果你需要能够对动物的类型进行案例处理,你会使用后者。如果你想要前者,但想要一个列表,你不必使用存在主义类型,你可以改为在列表中捕获你想要的东西,比如:

voicesOfAnimals :: [() -> String]
voicesOfAnimals = [_ -> voice Cat, _ -> voice Dog]

甚至更简单

voicesOfAnimals :: [String]
voicesOfAnimals = [voice Cat, voice Dog]

无论如何,这就是您对异构列表所做的,您有一个约束,在本例中Animal a每个元素,它允许您对每个元素调用voice,但没有其他内容,因为约束不会为您提供有关值的更多信息(好吧,如果您有约束Typeable a您将能够做更多的事情, 但是我们在这里不用担心动态类型)。


至于Haskell不支持没有扩展和包装器的异构列表的原因,我会让其他人解释一下,但关键主题是:

  • 子类型
  • 方差
  • 推理
  • https://gitlab.haskell.org/ghc/ghc/-/wikis/impredicative-polymorphism(我认为)

在您的 Java 示例中,Arrays.asList(new Cat())的类型是什么?好吧,这取决于您声明的内容。如果你用List<Cat>声明变量,它会进行类型检查,你可以用List<Animal>声明它,也可以用List<Object>声明它。如果您将其声明为List<Cat>,您将无法将其重新分配给List<Animal>因为这是不合理的。

在 Haskell 中,typeclass 不能用作列表中的类型(因此[Cat]在第一个示例中有效,[Animal]在第二个示例中有效,但在第一个示例中无效[Animal]),这似乎是由于 Haskell 不支持非谓词多态性(不是 100% 确定)。Haskell列表的定义类似于[a] = [] | a : [a]。[x, y, z] 只是x : (y : (z : []))的合成糖。所以考虑一下哈斯克尔的例子。假设您在 repl 中键入 [Dog](这相当于Dog : []顺便说一句)。Haskell推断这具有[狗]类型。但是如果你在前面给它 Cat,比如 [Cat, Dog] (Cat : Dog : []),它将匹配第二个构造函数(:),并且会推断出Cat : ...的类型[Cat]Dog : []无法匹配。

既然其他人已经解释了如何在很多情况下避免存在主义类型,我想我会指出为什么你可能想要它们。我能想到的最简单的例子叫做Coyoneda

data Coyoneda f a = forall x. Coyoneda (x -> a) (f x)

Coyoneda f a包含一个充满某种类型x的容器(或其他函子)和一个可以映射到其上以生成f a的函数。下面是Functor实例:

instance Functor (Coyoneda f) where
fmap f (Coyoneda g x) = Coyoneda (f . g) x

请注意,这没有Functor f约束!是什么让它有用?要解释这一点还需要两个功能:

liftCoyoneda :: f a -> Coyoneda f a
liftCoyoneda = Coyoneda id
lowerCoyoneda :: Functor f => Coyoneda f a -> f a
lowerCoyoneda (Coyoneda f x) = fmap f x

很酷的是,fmap应用程序一起构建和执行:

lowerCoyoneda . fmap f . fmap g . fmap h . liftCoyoneda

在操作上

fmap (f . g . h)

而不是

fmap f . fmap g . fmap h

如果基础函子中的fmap开销很大,这可能很有用。

最新更新