如果参数多态性在不依赖于参数类型的情况下进行调度,那么除了 arity 之外还有什么可以调度?如果不一样,有人可以提供一个反例吗?
1 回答
参数多态性
参数多态背后的想法是你不分派——一个参数多态函数是一个对所有输入类型都以相同方式表现的函数。让我们考虑一个非常简单的例子(我将使用 Haskell 1):
id x = x
这定义了一个名为的函数id
,它接受一个参数x
,并返回它。这是id实体函数;它什么也没做。现在,应该id
有什么类型?它绝对是一个函数,所以它会有一些和的类型。我们可以说有类型;then将评估为,但不会进行类型检查,这似乎很愚蠢。说也好不了;问题反过来了。我们知道对于,输入的类型无关紧要;忽略该结构,只是传递值。所以对于任何类型,都有类型input -> output
input
output
id
Int -> Int
id 3
3
id True
id :: Bool -> Bool
id
id
a
id
a -> a
,我们可以明确地写成这样:
id :: a -> a
id x = x
在 Haskell 中,类型中的小写标识符是普遍量化的变量——上面的签名和我写的一样id :: forall a. a -> a
,除了写forall
显式只对某些语言扩展有效。
恒等函数是参数多态函数的最简单示例,它强调了参数函数只是传递数据的想法。他们无法检查数据以对其进行任何处理。
让我们考虑一个更有趣的功能:列表反转。a
在 Haskell 中,编写了某种类型的列表[a]
,所以reverse
函数是
reverse :: [a] -> [a]
reverse [] = []
reverse (x:xs) = reverse xs ++ [x]
-- `x:xs` is the list whose first element is `x` and whose second element is
-- `xs`; `++` is the list-append operator.
该reverse
函数将列表中的元素打乱,但它从不操纵它们(因此从不“调度”它们)。因此,reverse
它知道它必须获取和返回某物的列表——但它并不关心那是什么。
参数多态的最后一个例子是map
函数。此函数接受一个函数f
和一个列表,并将该函数应用于列表中的每个元素。这个描述告诉我们,我们不关心函数的输入或输出类型,也不关心输入列表的类型——但它们必须适当地匹配。因此,我们有
map :: (a -> b) -> [a] -> [b]
map f [] = []
map f (x:xs) = f x : map f xs
-- In Haskell, function application is whitespace, so `map f xs` is like
-- `map(f,xs)` in a C-like language.
注意传入函数的输入(分别为输出)类型和输入(分别为输出)列表的元素类型必须匹配;但是,输入和输出类型可以彼此不同,我们不在乎。
参数多态性和子类型
您在评论中询问参数函数是否只接受顶级类型的值。答案是否定的:子类型完全独立于参数多态性。Haskell 根本没有任何子类型化的概念: anInt
是 an Int
,而 aBool
是 a Bool
,两者永远不会相遇。在 Java 中,你有泛型和子类型,但是这两个特性在语义上是不相关的(除非你使用form 的有界多态<T extends Super>
,但这更像是ad-hoc polymorphism的一种形式,我将在下面讨论)。参数多态性确实是它所说的:接受任何类型。这与接受顶级类型并依赖包含/隐式向上转换的函数不同。考虑它的一种方法是参数函数需要一个额外的参数:参数的类型。因此id 3
,您将拥有id Int 3
; 而不是id True
,你会有id Bool True
。在 Haskell 中,您永远不需要显式地执行此操作,因此它没有语法。另一方面,在 Java 中,有时您需要这样做,因此有反映这一点的语法,如Collections.<String>emptyList()
.
即席多态性
参数多态性通常与各种形式的即席多态性形成对比:多态性允许一个函数在不同类型下以不同方式表现。这就是“调度”出现的地方;其中参数多态性是关于一致性的,而临时多态性是关于差异性的。有时您不希望函数在每种类型下都以相同的方式运行!
标准的类似 Java 的面向对象的子类型多态,正式称为名义子类型,就是一个例子;例如,在 Java 中,该boolean Object.equals(Object)
方法使用子类型多态性来调度其第一个参数并返回适当的结果。很明显,您不希望平等是参数化的;你不能编写一个函数来准确地比较字符串和整数是否相等!但是请注意,这.equals
也用于instanceof
对参数的运行时类型执行“类型大小写”检查;该int Object.hashCode()
方法是纯子类型多态方法的一个示例。
Haskell 使用一种不同的方法,称为类型类多态性来处理这个问题。这是如何工作的旋风之旅。首先,我们说一下相等性的可比性意味着什么(请注意,Haskell 允许您定义任意运算符的函数名称,然后使用它们中缀):
class Eq a where -- To say that a type `a` is comparable for equality, implement
-- these functions:
(==) :: a -> a -> Bool -- Equality
(/=) :: a -> a -> Bool -- Inequality
-- We can also define default implementations for those functions:
x == y = not (x /= y)
x /= y = not (x == y)
然后,我们实例化类型类;例如,这里我们说如何比较布尔值是否相等。
instance Eq Bool where
True == True = True
False == False = True
_ == _ = False
-- `_` means "don't care".
当我们想比较元素是否相等时,我们指定我们必须有一个满足适当约束的类型。例如,elem
检查元素是否出现在列表中的函数类型为Eq a => a -> [a] -> Bool
; 我们可以将其理解为“对于任何a
一个 的实例Eq
,elem
期望一个a
和一个a
s 的列表,并返回一个布尔值”:
elem :: Eq a => a -> [a] -> Bool
elem _ [] = False
elem y (x:xs) = x == y || elem y xs
-- Haskell supports an infix syntax that would have allowed us to write
-- `y `elem` xs`, with the backticks around `elem`.
在这里,elem
函数不是参数多态的,因为我们有一些关于类型的信息a
——我们知道我们可以比较它的元素是否相等。因此,elem
不会对每种输入类型都以相同的方式表现(并且有些类型我们甚至无法比较相等性,例如函数),因此这里也有一种基于类型的调度形式。
1如果您更熟悉 Java 等语言,Java 中的相同功能将是(忽略包含类)
public static <T> T id(T t) { return t; }
请注意,与 Haskell 不同,Java 允许您通过使用instanceof
运算符或调用.toString()
始终可用的方法来违反参数性,但我们的id
函数不会这样做。