类型类(Typeclasses)和抽象数据类型(Abstract Data Types)有什么区别?
我知道这对于Haskell程序员来说是基本的东西,但我来自Scala背景,希望能举出Scala的例子。目前我所找到的最好的解释是,类型类是“开放”的,而ADT则是“封闭”的。与结构类型进行比较和对比也会很有帮助。
类型类(Typeclasses)和抽象数据类型(Abstract Data Types)有什么区别?
我知道这对于Haskell程序员来说是基本的东西,但我来自Scala背景,希望能举出Scala的例子。目前我所找到的最好的解释是,类型类是“开放”的,而ADT则是“封闭”的。与结构类型进行比较和对比也会很有帮助。
data Maybe a = Nothing | Just a
Option
:sealed trait Option[+T]
case class Some[T](value: T) extends Option[T]
case object None extends Option[Nothing]
Option
,但你能理解要点。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
的变量,它可以包含一个Leaf
或Branch
,你可以检查哪一个在那里并提取所包含的数据(如果需要)。这种检查和提取的主要手段是模式匹配:-- 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)})"
}
sealed
关键字实现的,该关键字禁止其他文件中的子类。Tree a = 1 + a * (Tree a) * (Tree a)
x
满足定义了某个操作的契约。然后,您可以调用该方法,实际执行该契约的实现将自动选择。-- 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"
}
class MyShow a where
myShow :: a -> String
instance MyShow Int where
myShow x = ...
class Read a where
read :: String -> a
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"
}
data Foo = Bar
这种类型只有一个值: Bar
。单独使用并不是非常有趣,我们需要一些方法来构建更大的类型。
第一种方法是给我们的构造函数传递参数。例如,我们可以让我们的Bar
接受一个整数和一个字符串:
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中,代数数据类型是封闭的,这意味着您不能在后期添加新的选择。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
_
只是表示我们不关心点的中心值。Map
(来自于Data.Map
)。该映射实际上是特定类型的二叉搜索树,但模块中没有任何内容让您直接使用该树结构。这很重要,因为树需要保持某些额外的不变性,您很容易弄乱它们。因此,您只能将Map
作为不透明的blob使用。Show
,它是具有字符串表示形式的所有类型的类;也就是说,对于我们有一个函数show ∷ a → String
的所有类型a
。如果类型具有show
函数,我们说它"在Show
中";否则,它不在其中。您所知道的大多数类型,如Int
、Bool
和String
都在Show
中;另一方面,函数(任何带有→
的类型)不在Show
中。这就是为什么GHCi无法打印函数的原因。Show
可以仅通过show
函数来定义。class Show a where
show ∷ a → String
Show
中添加一个名为Foo
的新类型,我们需要为其编写一个实例。以下是show
函数的实际实现:instance Show Foo where
show foo = case foo of
Bar → "Bar"
Baz → "Baz"
Foo
在 Show
中。我们可以在任何地方为 Foo
编写实例。特别是,在类已被定义之后,甚至在其他模块中也可以编写新的实例。这就是类型类“开放”的含义;与代数数据类型不同,我们可以在事后向类型类添加新内容。Show
还有另一个可能的函数,它将 列表 的 a
转换为 String
。这基本上是一种 hack,使字符串看起来漂亮,因为字符串只是 Char
的列表而不是它自己的类型。类型类和ADT的区别在于:
例如,考虑print
函数:
print :: (Show a) => a -> IO ()
类型是静态的,不能在程序的整个生命周期内改变,因此当您使用类型类时,基于调用点推断的类型,在编译时静态选择要使用的方法。因此,在这个例子中,我知道即使没有运行程序,我正在使用Show
的Char
实例:
main = print 'C'
抽象数据类型可以动态地改变函数的行为。例如,我可以定义:
print2 :: Either Char String -> IO ()
print2 (Left c ) = putStrLn [c]
print2 (Right str) = putStrLn str
print2
:print2 e
print2
使用哪个分支,除非我知道e
的运行时值。如果e
是Left
,那么我选择Left
分支;如果e
是Right
,那么我选择Right
分支。有时我可以静态地推断出e
将会是哪个构造器,但有时我无法推断,例如以下示例:main = do
e <- readLn -- Did I get a 'Left' or 'Right'?
print2 e -- Who knows until I run the program