多态是通常定义数据结构或算法的想法,因此您可以将它们用于多个数据类型。 完整的答案虽然有些微妙。 在这里,我收集了从您最可能已经使用过的常见类型到不太常见的常见类型的各种形式的多态性,并比较了它们在面向对象或功能语言中的外观。
Parametric polymorphism
这是许多语言中非常普遍的技术,尽管更好地称为“泛型”。 核心思想是允许程序员在定义以后可以用任何类型填充的数据结构时使用通配符类型。 例如,这是在Java中的外观:
class List<T> {
class Node<T> {
T data;
Node<T> next;
}
public Node<T> head;
public void pushFront(T data) { /* ... */ }
}
的Ť是类型变量,因为您以后可以“分配”所需的任何类型:
List<String> myNumberList = new List<String>();
myNumberList.pushFront("foo");
myNumberList.pushFront(8) // Error: 8 is not a string
这里的列表只能包含字符串类型的元素,不能包含其他任何元素。 如果尝试违反此规则,则会得到有用的编译器错误。 另外,我们不必再次为每种可能的数据类型定义列表,因为我们只需定义它即可对全部可能的类型。
但是不仅命令式或面向对象的语言具有参数多态性,而且在函数式编程中也很常见。 例如在Haskell中,列表的定义如下:
data List a = Nil | Cons a (List a)
该定义意味着:列表采用类型参数一种 (everything left of the equ一种ls sign defines the type) 一种nd is either 一种n empty list (零) or 一种n element of type 一种 一种nd 一种 list of type List 一种. We don't need 一种ny extern一种l pushFront method, bec一种use the second constructor 一种lre一种dy does this:
let emptyList = Nil
oneElementList = Cons "foo" emptyList
twoElementList = Cons 8 oneElementList -- Error: 8 is not a string
Ad-hoc polymorphism
这通常称为函数或运算符重载。 在允许这样做的语言中,您可以多次定义一个函数来处理不同的输入类型。 例如在Java中:
class Printer {
public String prettyPrint(int x) { /* ... */ }
public String prettyPrint(char c) { /* ... */ }
}
编译器将根据您传递给它的数据类型自动选择正确的方法。 这可以使API易于使用,因为您可以使用任何类型调用相同的函数,而不必记住针对不同类型的大量变体(àlaprint_string,print_int,etc).
在Haskell中,临时多态性通过类型类起作用。 类型类有点像面向对象语言中的接口。 例如,请参阅此处相同的漂亮打印机:
class Printer p where
prettyPrint :: p -> String
instance Printer Int where
prettyPrint x = -- ...
instance Printer Char where
prettyPrint c = -- ...
Subtype polymorphism
子类型被称为面向对象的继承。 经典示例是车辆类型,在Java中:
abstract class Vehicle {
abstract double getWeight();
}
class Car extends Vehicle {
double getWeight() { return 10.0; }
}
class Truck extends Vehicle {
double getWeight() { return 100.0; }
}
class Toyota extends Car { /* ... */ }
static void printWeight(Vehicle v) {
// Allowed because all vehicles have to have this method
System.out.println(v.getWeight());
}
在这里我们可以使用任何车辆类的儿童类仿佛那是直接的汽车课。 请注意,我们不能走另一条路,因为例如,并非保证每辆车都是一辆汽车。
当允许您传递函数时,例如在打字稿中,这种关系变得有些繁琐:
const driveToyota = (c: Toyota) => { /* ... */ };
const driveVehicle = (c: Vehicle) => { /* ... */ };
function driveThis(f: (c: Car) => void): void { /* ... */ }
您可以将两个函数中的哪一个传递给驾驶这? 您可能会认为第一个,毕竟我们已经在上面的函数中看到,该函数期望对象也可以传递其子类(请参见printWeight方法)。但如果您通过了,这是错误的功能。 您可以这样想:驾驶这想要一些可以接受的东西任何 car. 但 if you pass in 丰田, the 功能 can only deal with Toyotas which is not enough. On the other hand if you pass in a 功能 that can drive 任何 车辆(驾驶车辆),这也包括汽车,因此驾驶这会接受的。
由于Haskell不是面向对象的,因此子类型化没有多大意义。
Row polymorphism
现在我们来讨论仅用很少的语言实现的不常用的多态类型。
行多态性就像子类型的小兄弟。而不是说出表单中的每个对象{a::A,b::B}isalsoa{a::A},weallowtospecifyarowextension:{a::A|r}.Thismakesiteasiertouseinfunctions,asyoudonothavetothinkifyouareallowedtopassthemorespecificorthemoregeneraltype,butinsteadyoujustcheckifyoutypematchesthepattern.So{a::A,b::B}matches{a::A|r}but{b::B,c::C}doesnot.Thisalsohastheadvantagethatyoudonotloseinformation.Ifyoucasta汽车toa车辆youhavelosttheinformationwhichspecificvehicletheobjectwas.Withtherowextension,youkeepalltheinformation.
printX :: { x :: Int | r } -> String
printX rec = show rec.x
printY :: { y :: Int | r } -> String
printY rec = show rec.y
-- type is inferred as `{x :: Int, y :: Int | r } -> String`
printBoth rec = printX rec ++ printY rec
One of the most popular languages implementing row polymorphism is PureScript and I am also currently working on bringing it to Haskell.
Kind polymorphism
种类是类型的种类。 我们都知道值,它是每个函数处理的数据。5,“ foo”,假 一种re 一种ll ex一种mples of v一种lues. Then there is the type level,describing v一种lues. This should 一种lso be very known to progr一种mmers. The types of the three v一种lues before 一种re 整数,串和布尔。 但甚至还有一个更高的水平:种类。 所有类型的种类是类型也写成*。 所以这意味着5 :: 整数 :: 类型(:: me一种ns "h一种s type")。 There 一种re 一种lso other kinds. For ex一种mple while our list type e一种rlier is 一种 type(列出一个),wh一种t is 清单(without the 一种)? It still needs 一种nother type 一种s 一种rgument to form 一种 norm一种l type. Therefore its kind is 清单 :: 类型 -> 类型。 如果你给清单 一种nother type(for ex一种mple 整数) you get 一种 new type(清单 整数)。
种类多态是指您只能定义一次类型,但仍然可以将其用于多种类型。 最好的例子是代理Haskell中的数据类型。 它用于“标记”具有以下类型的值:
data Proxy a = ProxyValue
let proxy1 = (ProxyValue :: Proxy Int) -- a has kind `Type`
let proxy2 = (ProxyValue :: Proxy List) -- a has kind `Type -> Type`
Higher-rank polymorphism
有时,普通的即席多态性还不够。 使用临时多态性,您可以为不同类型提供许多实现,并且API的使用者可以选择他要使用的类型。 但是有时候,您作为API的生产者想要选择要使用的实现。 这是您需要更高级别的多态性的地方。 在Haskell中,如下所示:
-- ad-hoc polymorphism
f1 :: forall a. MyTypeClass a => a -> String
f1 = -- ...
-- higher-rank polymorphism
f2 :: Int -> (forall a. MyTypeClass a => a -> String) -> Int
f2 = -- ...
而不是拥有对全部在最外面的地方,我们将其向内推,因此声明:向我传递一个可以处理任何类型的函数一种 th一种t implements MyTypeCl一种ss。 You c一种n prob一种bly see th一种t f1 is such 一种 function, so you 一种re 一种llowed to p一种ss it to f2。
为什么有用的标准示例是所谓的“ ST-Trick”。 这是允许Haskell具有无法逃脱范围的可变状态的原因:
doSomething :: ST s Int
doSomething = do
ref <- newSTRef 10
x <- readSTRef ref
writeSTRef (x + 7)
readSTRef ref
的s这里的参数很重要,每个有状态操作都需要相同的参数s in the signature. 的magic is now the function that converts a stateful computation to a pure one, 运行ST:
runST :: forall a. (forall s. ST s a) -> a
你可以看到s是使用更高级别的多态创建的。 由于我们没有在什么的s is,的stateful computation cannot do anything with it except passing it around in的type signatures. It behaves just like的"tag" of 代理. And because it only defined in的scope of的stateful action (by的inner 对全部), every attempt at leaking anything from的computation is an error in的compiler.
Linearity polymorphism
线性多态性与线性类型(也称为跟踪数据“使用”的类型)相关。 线性类型跟踪某些数据的所谓多重性。 通常,您可以区分3种不同的多重性:“零”,用于仅在类型级别存在且不允许在值级别使用的内容; “一个”,表示不允许重复的数据(文件描述符为此示例),“许多”表示所有其他数据。
线性类型对于保证资源使用很有用。 例如,如果你折在使用功能语言的可变数组上。 通常,您将必须在每个步骤中复制阵列,或者必须使用低级的不安全功能。 但是,对于线性类型,您可以保证一次只能在一个位置使用此数组,因此不会发生数据争用。
多态性方面发挥了上述功能折. If you give 折 a function that uses its argument only once, the whole 折将仅使用一次初始值。 如果您传递的函数多次使用该参数,则初始值也将被多次使用。 Linarity多态性允许您仅定义一次功能,并且仍然提供此保证。
Linear types are similar to Rust's borrow checker, but Rust does not really have linearity polymorphism. Haskell is getting linear types and polymorphism soon.
Levity polymorphism
在Haskell中,所有普通数据类型都只是对堆的引用,就像在Python或Java中一样。 但是Haskell还允许使用所谓的“未提升”类型,例如,直接表示机器整数。 这意味着Haskell实际上在类型级别上对数据类型的内存布局和位置(堆栈或堆)进行编码! 这些可用于进一步优化代码,因此CPU不必先加载引用,然后再从(慢速)RAM请求数据。
左多态是指定义对提升类型和非提升类型都起作用的函数。