Haskell 中的存在类型和其他语言中的泛型
Existential types in Haskell and generics in other languages
我试图通过文章 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
我对“异构列表”有不同的概念,类似于 Shapeless in Scala。但在这里,它只是包裹在只添加类型约束的存在类型中的项目列表。它的元素的确切类型并没有体现在它的类型签名中,我们唯一知道的是它们都符合类型约束。
在面向对象的语言中,写这样的东西似乎很自然(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 倍长。
is there just no basic-level use cases for this?
差不多,是的。在 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不支持没有扩展和包装器的异构列表,我会让其他人解释,但关键主题是:
在您的 Java 示例中,Arrays.asList(new Cat())
的类型是什么?好吧,这取决于您将其声明为什么。如果你用 List<Cat>
声明变量,它会进行类型检查,你可以用 List<Animal>
声明它,你也可以用 List<Object>
声明它。如果您将其声明为 List<Cat>
,您将无法将其重新分配给 List<Animal>
,因为那样是不合理的。
在 Haskell 中,type类 不能用作列表中的类型(因此 [Cat]
在第一个示例中有效, [Animal]
有效在第二个示例中,但 [Animal]
在第一个示例中无效),这似乎是由于 Haskell 中不支持谓词多态性(并非 100% 确定)。 Haskell 列表的定义类似于 [a] = [] | a : [a]
。 [x, y, z] 只是 x : (y : (z : []))
的语法糖。因此请考虑 Haskell 中的示例。假设您在 repl 中键入 [Dog](这相当于 Dog : []
btw)。 Haskell 推断其具有 [Dog] 类型。但是如果你在前面给它 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
在底层仿函数中开销很大,这会很有用。
我试图通过文章 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
我对“异构列表”有不同的概念,类似于 Shapeless in Scala。但在这里,它只是包裹在只添加类型约束的存在类型中的项目列表。它的元素的确切类型并没有体现在它的类型签名中,我们唯一知道的是它们都符合类型约束。
在面向对象的语言中,写这样的东西似乎很自然(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 倍长。
is there just no basic-level use cases for this?
差不多,是的。在 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不支持没有扩展和包装器的异构列表,我会让其他人解释,但关键主题是:
在您的 Java 示例中,Arrays.asList(new Cat())
的类型是什么?好吧,这取决于您将其声明为什么。如果你用 List<Cat>
声明变量,它会进行类型检查,你可以用 List<Animal>
声明它,你也可以用 List<Object>
声明它。如果您将其声明为 List<Cat>
,您将无法将其重新分配给 List<Animal>
,因为那样是不合理的。
在 Haskell 中,type类 不能用作列表中的类型(因此 [Cat]
在第一个示例中有效, [Animal]
有效在第二个示例中,但 [Animal]
在第一个示例中无效),这似乎是由于 Haskell 中不支持谓词多态性(并非 100% 确定)。 Haskell 列表的定义类似于 [a] = [] | a : [a]
。 [x, y, z] 只是 x : (y : (z : []))
的语法糖。因此请考虑 Haskell 中的示例。假设您在 repl 中键入 [Dog](这相当于 Dog : []
btw)。 Haskell 推断其具有 [Dog] 类型。但是如果你在前面给它 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
在底层仿函数中开销很大,这会很有用。