为什么(或为什么不这样)存在性类型在函数式编程中被认为是不好的做法?


43

我可以使用哪些技术来一致地重构代码,以消除对存在类型的依赖?通常,这些用于取消您不希望使用的类型的构造的资格,并允许在对给定类型有最少了解的情况下进行消耗(或者我的理解是这样)。

有没有人想出一种简单而一致的方法来消除对代码中这些内容的依赖,而这些代码仍然保留了一些好处?还是至少有任何一种滑入抽象的方式,使得它们可以删除而无需大量的代码改动来应对这种变更?

您可以在此处阅读有关存在性类型的更多信息(“如果您敢..”)。


8
@RobertHarvey我找到了使我第一次思考这个问题的博客文章:Haskell Antipattern:Existential Typeclass。另外,乔伊·亚当斯(Joey Adams)提到的论文在第3.1节中描述了一些存在性问题。如果您有相反的论点,请分享。
PetrPudlák13年

5
@PetrPudlák:请记住,反模式通常不存在类型,但是当更简单的事情(在Haskell中得到更好的支持)可以完成相同的工作时,则专门使用它们。
CA McCann

10
如果您想知道特定博客帖子的作者为什么发表意见,那么您可能应该问的人就是作者。
埃里克·利珀特

6
@Ptolemy这是一个见解的问题。使用Haskell多年,我几乎无法想象使用没有强类型系统的功能语言。
PetrPudlák2015年

4
@托勒密:Haskell的口头禅是“如果编译成功”,Erlangs口头禅是“让它崩溃”。动态类型作为胶水很好,但是我个人不会仅使用胶水来构建东西。
书斋

Answers:


8

存在类型在函数式编程中并不是真正的坏习惯。我认为让您感到震惊的是,存在性最常被引用的用途之一是存在性类型类反模式,许多人确实认为这是不好的做法。

这种模式经常被淘汰,作为对如何拥有一个均实现相同类型类的异构类型元素列表的回答。例如,您可能想要一个包含Show实例的值的列表:

{-# LANGUAGE ExistentialTypes #-}

class Shape s where
   area :: s -> Double

newtype Circle = Circle { radius :: Double }
instance Shape Circle where
   area (Circle r) = pi * r^2

newtype Square = Square { side :: Double }
    area (Square s) = s^2

data AnyShape = forall x. Shape x => AnyShape x
instance Shape AnyShape where
    area (AnyShape x) = area x

example :: [AnyShape]
example = [AnyShape (Circle 1.0), AnyShape (Square 1.0)]

像这样的代码的问题是这样的:

  1. 您可以在上执行的唯一有用的操作AnyShape是获取其面积。
  2. 您仍然需要使用AnyShape构造函数将一种形状类型引入AnyShape类型。

事实证明,这段代码并没有真正让您得到此较短的代码所没有的任何东西:

class Shape s where
   area :: s -> Double

newtype Circle = Circle { radius :: Double }
instance Shape Circle where
   area (Circle r) = pi * r^2

newtype Square = Square { side :: Double }
    area (Square s) = s^2

example :: [Double]
example = [area (Circle 1.0), area (Square 1.0)]

对于多方法类,通常可以通过使用“方法的记录”编码来更简单地实现相同的效果,而不是使用像这样的类型类Shape,您可以定义一个字段为该方法的“方法”的记录Shape类型。 ,然后编写函数将圆和正方形转换为Shapes。


但这并不意味着存在类型是一个问题!例如,在Rust中,它们具有称为特征对象的功能,人们通常将其描述为特征上的存在类型(Rust的类型类版本)。如果存在类型类是Haskell中的反模式,这是否意味着Rust选择了错误的解决方案?没有!Haskell世界的动机是语法和便利性,而不是原则。

一种更数学的说法是指出AnyShape上面的类型Double同构的 —它们之间存在“无损转换”(嗯,除了浮点精度外):

forward :: AnyShape -> Double
forward = area

backward :: Double -> AnyShape
backward x = AnyShape (Square (sqrt x))

因此严格来说,选择一个与另一个不会获得或失去任何力量。这意味着选择应该基于其他因素,例如易用性或性能。


并且请记住,存在类型在此异构列表示例之外还有其他用途,因此最好使用它们。例如,Haskell的ST类型允许我们编写外部纯函数但内部使用内存突变操作的函数,它使用基于存在类型的技术来保证编译时的安全性。

因此,普遍的答案是没有普遍的答案。存在类型的使用只能在上下文中进行判断-答案可能会有所不同,具体取决于不同语言提供的功能和语法。


那不是同构。
赖安·赖希

2

我对Haskell不太熟悉,因此我将以非学术性C#开发人员的身份回答问题的一般部分。

经过阅读后,结果是:

  1. Java通配符类似于存在性类型:

    例如,Scala的存在类型和Java的通配符之间的区别

  2. 通配符不是完全在C#中实现的:支持泛型方差,但不支持调用站点方差:

    C#泛型:通配符

  3. 您可能并非每天都需要此功能,但是当您执行此操作时,您会感觉到它的存在(例如,必须引入额外的类型才能使事情正常运行):

    C#通用约束中的通配符

基于此信息,存在类型/通配符在正确实现时很有用,它们本身没有任何问题,但是它们可能像其他语言功能一样被滥用。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.