【问题标题】:Scala: difference between a typeclass and an ADT?Scala:类型类和 ADT 之间的区别?
【发布时间】:2013-10-05 14:08:38
【问题描述】:

类型类和抽象数据类型有什么区别?

我意识到这对于 Haskell 程序员来说是一件基本的事情,但我来自 Scala 背景,并且会对 Scala 中的示例感兴趣。我现在能找到的最好的是类型类是“开放的”而 ADT 是“封闭的”。将类型类与结构类型进行比较和对比也会有所帮助。

【问题讨论】:

  • 你的意思是代数数据类型吗?
  • 对不起,我猜在 OO 世界中,ADT 往往意味着抽象数据类型,但在 FP 世界中,它意味着代数数据类型,所以我有点困惑。感谢大家解决这个问题。

标签: scala haskell typeclass abstract-data-type structural-typing


【解决方案1】:

类型类和ADT的区别是:

  • 当您想根据某事物的类型调度方法时,请使用类型类
  • 当您想根据某事物的调度方法时,请使用 ADT

例如,考虑print 函数:

print :: (Show a) => a -> IO ()

类型是静态的,在程序的整个生命周期内都不能更改,因此当您使用类型类时,您使用的方法是在编译时根据调用站点推断的类型静态选择的。所以在这个例子中,我知道我在使用ShowChar 实例,甚至没有运行程序:

main = print 'C'

ADT 让您可以动态地更改函数的行为。例如,我可以定义:

print2 :: Either Char String -> IO ()
print2 (Left  c  ) = putStrLn [c]
print2 (Right str) = putStrLn str

现在,如果我在某些情况下调用 print2

print2 e

...除非我知道e 的运行时值,否则我无法知道print2 采用哪个分支。如果eLeft,那么我采用Left 分支,如果eRight,那么我采用Right 分支。有时我可以静态推断e 将是哪个构造函数,但有时我不能,例如在以下示例中:

main = do
    e <- readLn  -- Did I get a 'Left' or 'Right'?
    print2 e     -- Who knows until I run the program

【讨论】:

    【解决方案2】:

    ADT(在这种情况下不是抽象数据类型,这甚至是另一个概念,而是代数数据类型)和类型类是完全不同的概念,它们解决不同的问题。

    ADT,如下的首字母缩写词,是一种数据类型。需要 ADT 来构建数据。我认为,Scala 中最接近的匹配是案例类和密封特征的组合。这是在 Haskell 中构建复杂数据结构的主要方法。我认为 ADT 最著名的例子是 Maybe 类型:

    data Maybe a = Nothing | Just a
    

    这种类型在标准 Scala 库中有一个直接等价物,称为 Option

    sealed trait Option[+T]
    case class Some[T](value: T) extends Option[T]
    case object None extends Option[Nothing]
    

    Option 在标准库中的定义并不完全如此,但您明白了。

    基本上 ADT 是几个命名元组的组合(在某种意义上)(0 元,如 Nothing/None;1 元,如 Just a/Some(value);更高的元组也是可能的) .

    考虑以下数据类型:

    -- Haskell
    data Tree a = Leaf | Branch a (Tree a) (Tree a)
    
    // Scala
    sealed trait Tree[+T]
    case object Leaf extends Tree[Nothing]
    case class Branch[T](value: T, left: Tree[T], right: Tree[T]) extends Tree[T]
    

    这是简单的二叉树。这两个定义基本上如下:“二叉树要么是Leaf,要么是Branch;如果它是一个分支,那么它包含一些值和另外两棵树”。这意味着如果您有一个Tree 类型的变量,那么它可以包含LeafBranch,如果需要,您可以检查哪个变量存在并提取包含的数据。这种检查和提取的主要手段是模式匹配:

    -- Haskell
    showTree :: (Show a) => Tree a -> String
    showTree tree = case tree of
      Leaf                    -> "a leaf"
      Branch value left right -> "a branch with value " ++ show value ++ 
                                 ", left subtree (" ++ showTree left ++ ")" ++
                                 ", right subtree (" ++ showTree right ++ ")"
    
    // Scala
    def showTree[T](tree: Tree[T]) = tree match {
      case Leaf => "a leaf"
      case Branch(value, left, right) => s"a branch with value $value, " +
                                         s"left subtree (${showTree(left)}), " +
                                         s"right subtree (${showTree(right)})"
    }
    

    这个概念很简单,但也很强大。

    正如您所注意到的,ADT 是封闭的,即在定义类型之后您不能添加更多命名元组。在 Haskell 中,这是在语法上强制执行的,而在 Scala 中,这是通过 sealed 关键字实现的,它不允许在其他文件中使用子类。

    这些类型被称为代数是有原因的。命名元组可以被认为是产品(在数学意义上)和这些元组的“组合”作为总和(也在数学意义上),这种考虑具有深刻的理论意义。比如前面提到的二叉树类型可以这样写:

    Tree a = 1 + a * (Tree a) * (Tree a)
    

    但我认为这超出了这个问题的范围。如果您想了解更多,我可以搜索一些链接。


    另一方面,类型类是定义多态行为的一种方式。大致类型类是某种类型提供的契约。例如,您知道您的值 x 满足定义某些操作的合同。然后您可以调用该方法,然后自动选择该合约的实际实现。

    通常将类型类与 Java 接口进行比较,例如:

    -- Haskell
    class Show a where
        show :: a -> String
    
    // Java
    public interface Show {
        String show();
    }
    
    // Scala
    trait Show {
      def show: String
    }
    

    使用这种比较,类型类的实例与接口的实现相匹配:

    -- Haskell
    data AB = A | B
    
    instance Show AB where
      show A = "A"
      show B = "B"
    
    // Scala
    sealed trait AB extends Show
    case object A extends AB {
      val show = "A"
    }
    case object B extends AB {
      val show = "B"
    }
    

    接口和类型类之间有非常重要的区别。首先,您可以编写自定义类型类并让 any 类型成为它的实例:

    class MyShow a where
      myShow :: a -> String
    
    instance MyShow Int where 
      myShow x = ...
    

    但是你不能用接口做这样的事情,也就是说,你不能让一个现有的类实现你的接口。正如您也注意到的,这个特性意味着类型类是开放的

    这种为现有类型添加类型类实例的能力是解决expression problem 的一种方法。 Java 语言没有解决它的方法,但 Haskell、Scala 或 Clojure 有。

    类型类和接口之间的另一个区别是接口仅在第一个参数上是多态的,即在隐式this 上。类型类在这个意义上不受限制。您可以定义甚至在返回值上分派的类型类:

    class Read a where
      read :: String -> a
    

    使用接口是不可能做到这一点的。

    可以使用隐式参数在 Scala 中模拟类型类。这种模式非常有用,以至于在最近的 Scala 版本中甚至有一种特殊的语法来简化它的使用。以下是它的完成方式:

    trait Showable[T] {
      def show(value: T): String
    }
    
    object ImplicitsDecimal {
      implicit object IntShowable extends Showable[Int] {
        def show(value: Int) = Integer.toString(value)
      }
    }
    
    object ImplicitsHexadecimal {
      implicit object IntShowable extends Showable[Int] {
        def show(value: Int) = Integer.toString(value, 16)
      }
    }
    
    def showValue[T: Showable](value: T) = implicitly[Showable[T]].show(value)
    // Or, equivalently:
    // def showValue[T](value: T)(implicit showable: Showable[T]) = showable.show(value)
    
    // Usage
    {
      import ImplicitsDecimal._
      println(showValue(10))  // Prints "10"
    }
    {
      import ImplicitsHexadecimal._
      println(showValue(10))  // Prints "a"
    }
    

    Showable[T] trait 对应类型类,隐式对象定义对应其实例。

    如您所见,类型类是一种接口,但功能更强大。您甚至可以选择类型类的不同实现,而使用它们的代码保持不变。然而,这种能力是以样板和额外实体为代价的。

    请注意,可以编写与上述 Scala 程序等效的 Haskell 程序,但需要编写多个模块或 newtype 包装器,因此我不在这里展示。

    顺便说一句,Clojure,一种在 JVM 上运行的 Lisp 方言,有 protocols,它结合了接口和类型类。协议在单个第一个参数上分派,但您可以为任何现有类型实现协议。

    【讨论】:

    【解决方案3】:

    您的问题实际上涉及三个不同的概念: 类型类、抽象数据类型和代数数据类型。 令人困惑的是,“抽象”和“代数”数据类型都可以 缩写为“ADT”;在 Haskell 上下文中,ADT 几乎总是意味着 “代数”。

    让我们定义所有三个术语。

    代数数据类型 (ADT),是一种可以通过组合 更简单的类型。这里的核心思想是一个“构造函数”,它是一个 定义值的符号。把它想象成一个值 Java 风格的枚举,除了它也可以带参数。最简单的 代数数据类型只有一个没有参数的构造函数:

    data Foo = Bar
    

    这种类型只有一个¹值:Bar。就其本身而言,这并不是很有趣。我们需要一些方法来构建更大的类型。

    第一种方法是给我们的构造函数参数。例如,我们可以让 Bars 接受一个 int 和一个字符串:

    data Foo = Bar Int String
    

    现在Foo 有许多不同的可能值:Bar 0 "baz"Bar 100 "abc" 等等。一个更现实的例子可能是员工的记录,看起来像这样:

    data Employee = Employee String String Int
    

    另一种构建更复杂类型的方法是让多个构造函数可供选择。例如,我们可以同时拥有Bar Baz

    data Foo = Bar
             | Baz
    

    现在Foo 类型的值可以是Bar Baz。这实际上是完全布尔值的工作方式; Bool 定义如下:

    data Bool = True
              | False
    

    它完全按照您的预期工作。真正有趣的类型可以使用这两种方法来组合自己。作为一个相当人为的例子,想象一下形状:

    data Shape = Rectangle Point Point
               | Circle Point Int
    

    形状可以是由两个角定义的矩形,也可以是一个圆心和一个半径的圆。 (我们只需将Point 定义为(Int, Int)。)很公平。但是在这里,我们遇到了一个障碍:原来其他形状也存在!如果某些相信三角形的异端想要在他们的模型中使用我们的类型,他们可以在事后添加Triangle 构造函数吗?不幸的是:在 Haskell 中,代数数据类型是封闭,这意味着你不能在事后添加新的替代方案。

    您可以对代数数据类型做的一件重要事情是模式匹配。这基本上意味着能够在 ADT 的替代方案上进行分支。作为一个非常简单的示例,您可以在 Bool 上进行模式匹配,而不是使用 if 表达式:

    case myBool of
      True  → ... -- true case
      False → ... -- false case
    

    如果你的构造函数有参数,你也可以通过模式匹配来访问这些值。使用上面的Shape,我们可以写一个简单的area函数:

    area shape = case shape of
      Rectange (x₁, y₁) (x₂, y₂) → (x₂ - x₁) * (y₂ - y₁)
      Circle _ r                 → π * r ^ 2
    

    _ 只是表示我们不关心点中心的值。

    这只是对代数数据类型的一个基本概述:事实证明还有更多的乐趣。您可能想查看 Learn You a Haskell(简称 LYAH)中的relevant chapter 以了解更多信息。

    现在,abstract 数据类型呢?这是指不同的概念。抽象数据类型是不公开实现的类型:您不知道该类型的值实际上是什么样的。你唯一能用它做的就是应用从它的模块导出的函数。您不能对其进行模式匹配或自己构造新值。实践中的一个很好的例子是Map(来自Data.Map)。映射实际上是一种特殊的二叉搜索树,但模块中的任何内容都不允许您直接使用树结构。这很重要,因为树需要维护某些您很容易搞砸的额外不变量。因此,您只能将Map 用作不透明的blob。

    代数和抽象类型在某种程度上是正交的概念;不幸的是,他们的名字很容易让人误认为另一个。

    拼图的最后一块是类型类。与代数和抽象数据类型不同,类型类本身不是类型。相反,可以将类型类视为类型的set。具体来说,类型类是实现特定功能的所有类型的集合。

    最简单的例子是Show,它是所有具有字符串表示的类型的类;也就是说,我们有函数show ∷ a → String 的所有类型a。如果一个类型有一个show 函数,我们说它是“in Show”;否则,它不是。您知道的大多数类型,如IntBoolString 都在Show 中;另一方面,函数(带有 的任何类型)在Show不是。这就是 GHCi 无法打印函数的原因。

    类型类是由一个类型需要实现哪些功能来定义的。例如,Show 可以仅由 show 函数定义²:

    class Show a where
      show ∷ a → String
    

    现在要将Foo 这样的新类型添加到Show,我们必须为其编写一个实例。这是show函数的实际实现:

    instance Show Foo where
      show foo = case foo of
        Bar → "Bar"
        Baz → "Baz"
    

    在此之后,FooShow 中。我们可以在任何地方为Foo 编写一个实例。特别是,我们可以在定义类之后编写新实例,甚至在其他模块中。这就是类型类开放的含义;与代数数据类型不同,我们可以在事后向类型类添加新内容。

    类型类还有更多;您可以在same LYAH chapter 中了解它们。

    ¹ 从技术上讲,还有另一个值称为 ⊥(底部),但我们现在将忽略它。您可以稍后了解⊥。

    ² 实际上,Show 实际上还有另一个可能的函数,它将as 的列表 转换为String。这基本上是一种使字符串看起来更漂亮的技巧,因为字符串只是Chars 的列表,而不是它自己的类型。

    【讨论】:

      猜你喜欢
      • 2014-02-26
      • 2012-12-09
      • 2010-12-17
      • 2016-09-28
      • 2015-12-20
      • 2013-11-17
      • 2021-01-12
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多