【问题标题】:Existential types in Haskell and generics in other languagesHaskell 中的存在类型和其他语言中的泛型
【发布时间】:2021-05-21 02:22:51
【问题描述】:

我试图通过文章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 中需要语言扩展、自定义语法并创建额外的包装器类型?不使用存在类型就没有办法实现类似的目标,还是没有基本级别的用例?

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

【问题讨论】:

  • 请注意,像上面那样混合类型类和存在通常是antipattern,因为通常存在更简单的等效表示。
  • Haskell“扩展”经常被误解为“额外的东西”或“不是语言的一部分”,但事实并非如此。您不妨说泛型不是 C#“真正”的一部分,因为它们只出现在 C# 2.0 中。 Haskell 仅允许您控制正在使用的功能,仅此而已。
  • 在大多数 OOP 语言中,几个特性被耦合在一起——命名空间、数据类型、存在多态性、子类型化、类型导向的动态调度、扩展/重用、封装……——并合并为一个抽象,。 Haskell 拥有这些概念中的大部分,它只是将它们组织为单独的特性——函数、模块、类型类、各种扩展......——我们只使用我们需要的部分。 Language 标志不一定是高级的,它们只是选择加入/明确的。我们通常使用存在性和 GADT 作为依赖类型的有限形式,以静态方式表示动态信息。

标签: haskell existential-type


【解决方案1】:

是的,存在类型和泛型意味着不同的东西。存在类型可以与面向对象语言中的接口类似地使用。当然,您可以将一个放在列表中,但使用接口不需要列表或任何其他泛型类型。有一个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倍。

【讨论】:

    【解决方案2】:

    没有基本的用例吗?

    有点,是的。在 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 中,类型类不能用作列表中的类型(因此 [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],即@ 987654345@ 将无法匹配。

    【讨论】:

    • 谢谢,很好的解释。
    【解决方案3】:

    由于其他人已经解释了如何在许多情况下避免存在类型,所以我想我会指出您可能需要它们的原因。我能想到的最简单的例子叫做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 在底层函子中很昂贵,这将很有用。

    【讨论】:

    • 非常有趣的用例,谢谢!
    • @Sergei,它们还出现在免费的应用函子、类型对齐的列表中,在您需要的各种设置中,例如,F x 和有关 x 的一些信息(概括Coyoneda 概念,你可以说)等等。
    • 返回类型can be considered existential中没有出现的任何类型变量。 flip :: (a->b->c) -> (b->a->c) 的类型返回 c 表示 ab 都是存在类型。它们可以打包data Flip c where MkFlip :: (a->b->c) -> b -> a -> Flip cflip 成为解包Flip c -> c 的函数,其中 ab 无处可见。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-12-25
    • 2022-10-14
    • 2011-08-15
    • 1970-01-01
    • 1970-01-01
    • 2021-10-21
    相关资源
    最近更新 更多