微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

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

如何解决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

我对“异构列表”有不同的看法,例如 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 = (drawCicrle center radius,pi * radius * radius)
rectangle topLeft bottomRight = (drawRectangle topLeft bottomRight,abs $ (topLeft.x-bottomRight.x) * (topLeft.y-bottomRight.y))

shapes = [circe (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 不支持没有扩展和包装器的异构列表的原因,我会让其他人解释一下,但关键主题是:

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

在 Haskell 中,typeclasses 不能用作列表中的类型(因此 [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 : [] 将无法匹配。

,

因为其他人已经解释了如何在许多情况下避免存在类型,所以我想我会指出您可能需要它们的原因。我能想到的最简单的例子叫做 constructor({ idKey,sortKey,}: BaseStoreConfig<T,Tid,Tsk>) { this[_idKey] = idKey ?? "id" // okay this[_sortKey] = sortKey } :

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 在底层函子中开销很大,这会很有用。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。