论Haskell的复用性(上篇)——多态

Posted Haskell慢慢谈

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了论Haskell的复用性(上篇)——多态相关的知识,希望对你有一定的参考价值。

    可复用的代码对于一个软件项目的开发总是会起到事半功倍的效果。一方面,作者可以用更精炼的代码解决问题;更为重要的另一方面是,复用代码对于维护者更加容易理解。在软件开发中,代码作者与维护者不同是常有的事情。维护者花费一段时间和精力理解了某个函数,然后发现这个函数在多处复用,从而自己也可以“复用”对该函数的理解而更快理解其它代码,这是多么令人身心愉悦的事情。与之相反,在看到有如散文一样信马由缰,以复制粘贴为复用基本方法的代码时,维护者在心里定会咒骂这个作者。


要写出结构精巧,高度复用的代码,作者对问题的提炼与抽象固然必不可少,编程语言能为代码复用提供多少技术支持也很重要。试想如果是用汇编语言编程,则再多的分解与抽象也将无用武之地。不同的语言之间,复用的实现方式也不尽相同,要为众多语言定一个令人信服的复用性的绝对排名显然是不可能的,也超出我的能力。在此仅就Haskell与C++两种语言基于我的理解做一个定性的比较。这是基于两个原因。首先,C++是我最熟悉的语言,没有之一。其次,C++与Haskell两者都是“类型语言”。C++的变量与Haskell中的符号都有类型,且不同类型的变量或符号之间不能直接赋值或关联。这使得二者的代码复用机制有一定相似之处,便于比较。


1 语言的复用性


语言的复用性这个概念过于笼统。我们需要先对其进一步细分,才好对Haskell和C++两种语言做逐项比较。


首先,数据结构和算法是高级编程语言的两大基本要素。据此,我们可以将代码复用分成三种:

  复用
1 数据结构 复用 数据结构
2 算法 复用 数据结构
3 算法 复用 算法

这里所说的甲复用乙,是指在甲的代码中利用了乙的代码。以下讨论中,我们用甲乙分别指代复用方代码与被复用方代码。另外,此处的数据结构是纯粹的数据的结构化描述,不包含如面向对象设计中的成员函数那样定义在数据结构中的算法。所以,不存在数据结构复用算法的情况。


复用关系还可以继续细分为特定与非特定的两种复用机制,两者的区别在于乙的定义对于甲是否已知,如是则为特定复用,否则为非特定复用。


特定复用是编程语言的非常基本的功能。例如在定义某个复杂结构体时用到另一个结构体的定义,即为数据结构特定复用另一数据结构;在函数中调用另一个函数即为算法特定复用另一算法;而在函数中利用某个复杂结构体保存数据或函数参数,即为算法特定复用数据结构。C++和Haskell都具备以上这些基本功能,二者在特定复用上不分高下,在此不做过多讨论。


非特定复用中,乙的定义对甲是未知的。甲的代码只能对乙的定义做出某种假设(例如假设乙是某种存在大小关系的数据如整数,浮点数,字符或者按字典顺序排列的字符串),并进行演算(如返回两个乙数据中的最小值)。


特定复用可以说是甲与有限的乙之间的复用关系,而非特定复用由于乙方代码不确定,是甲方与(理论上)无限的乙方代码之间的复用关系,其所定义的复用关系要远远大于特定复用之所为。要比较语言的复用性,需要对其非特定复用功能着重讨论。


2 C++泛型编程及其痼疾


我们首先讨论对数据结构的非特定复用。在类型语言中,这属于泛型编程(Generic Programming)的范畴。


C++的泛型编程是通过以类型为参数的类模板和函数模板实现的。乙方数据结构由模板的类型参数指代。任何满足模板的隐含要求的类型都可以应用于甲方的模板代码而生成对应的模板实例。例如在例1的代码中,函数模板sqsum返回任意类型的两个数的平方和,可用于计算整数、浮点数、复数或者有理数的平方和。

template<typename T>
T const & sqsum(T const &v1, T const &v2) {
 return v1 * v1 + v2 * v2;
}

C++的模板机制有一个很恼人的缺陷,就是缺乏对类型参数要求的成文描述。例1中的平方和函数sqsum中要对两个函数参数做乘法和加法,这实际上要求函数参数v1v2的类型,即模板的类型参数T,必须支持乘法和加法运算,否则编译将会出错。但是,由于C++的类型模板参数都只是用关键字typenameclass标识,根本无法反映如上的函数对模板参数的隐含要求。这将导致编译器无法在早期锁定错误,也很难在编译出错时给出正确的错误原因。

using std::string;
sqsum(string("abc"), string("def"));

在例2中,我们将std::string值(即字符串)传给sqsum函数,这显然是不正确的,因为std::string本身并不支持乘法和加法运算(除非用户自己另行定义)。但是,GCC编译例2代码时会将错误定位在sqsum函数内,这显然是一种误导。


不幸的是,C++的标准库中存在大量深度依赖模板的代码。这些代码一但用错,将产生大量的编译报错信息。这些错误信息对于没有经验的C++程序员来说不啻于天书。这使得模板编程以及很多标准库中的模板代码,虽然可称得上是C++的强力工具,却被大多数不熟悉模板的程序员视为畏途,敬而远之。


虽然C++界内曾经尝试为模板参数增加成文的要求,但这一提案最终被新一代的C++11标准排除在外,似乎也不在C++14计划之中。那么,C++模板的这一痼疾在近期治愈无望。


3 Haskell的多态


Haskell拥有一种与C++模板类似的机制——多态(Polymorphism)。如果一个函数的类型定义(函数的类型由其输入参数类型及返回值类型决定)中包含参数,则函数是一个多态函数。例3的代码演示了在Haskell中如何利用多态定义一个与例1类似的sqsum函数。

sqsum :: Num a => a -> a -> a
sqsum x y = x * x + y * y

为了将sqsum定义为一个多态函数,我们必须明确定义sqsum的类型。在Haskell中类型声明由“::”引导,所以sqsum的类型为Num a => a -> a ->a。定义中的a是类型参数,a -> a -> a现在先可以简单地理解为一个双输入单输出的函数类型,且输入输出数值皆为某种类型a。类型中的剩余部分Num a =>则约束类型参数a必须满足类型族(type class)Num的需求。这就是Haskell对类型参数需求的描述方法,也是Haskell多态比C++模板更加完备的一个重要依据。


Num是Haskell中预定义的类型族,例4中列出了它的部分定义。

例4

class Num a where
    (+), (-), (*) : a -> a -> a
    negate : a -> a
abs : a -> a

可见,满足Num要求的类型必须可以进行加法和乘法运算。Haskell的若干预定义数值类型如IntWord已经是Num的成员,而用户自定义类型也可以成为Num的成员,只要为其实现Num所需要的各种函数即可。


类型族从两方面避免了C++模板的痼疾。一方面,类型族约束了多态函数中可能进行的运算。编译器在编译多态函数时,可以利用类型族进行检查,尽早定位函数内部的错误。例如根据sqsum的类型定义可知其输入xy的类型为a。在sqsum中只能对xy进行Num中所定义的加法和乘法运算,却不能进行除法运算,因为Num并不支持除法(除法是定义在另一个类型族Fractional中)。


另一方面,在将多态函数应用于某个类型的参数时,编译器也会检查参数类型是否为对应类型族成员。如否,则可以给出明确的错误信息。例如将sqsum应用于两个字符串时,

sqsum "abc" "def"

由于String不是Num类型族的成员,编译器将直接报错,指出该错误,而不用像C++的编译器那样直到编译乘法和加法表达式时才发现有错,并给出误导的报错信息。


可见,由于类型族的存在,Haskell的泛型编程机制要比C++更为完善。从代码复用的角度审视,在对数据类型的非特定复用上,Haskell要略胜于C++。但这只是二者在代码复用差距中的一小部分。更为显著的差距,出现在我们尚未讨论过的算法对算法的非特定复用上。


4 附篇


4.1 没有类型族约束可以吗?


当然可以。Haskell并不强制要求类型族约束。对于一个多态函数,其可适用的类型范围越广,则可进行的操作就越少。没有类型族约束的多态函数,必须适用于任何类型,则其对类型也将无法做任何操作。但是,还是可以定义出这样的函数的,例如例5中的truefalse,就是两个没有类型族约束的a->a->a型的多态函数。

例5

true :: a -> a -> a
true x y = x

false :: a -> a -> a
false x y = y


4.2 如何识别类型参数?


上文中我们只是直接指出a -> a -> a中的a是一个类型参数。假如将a替换成int,那么在int -> int -> int类型中的int到底是一种类型,还是一种类型参数呢?答案是,int依然是一个类型参数。


Haskell为了消除这种歧义,特别规定类型标识符必须以大写字母开头。而所有以小写字母开头的标识符,按照Haskell标准中的术语,则是“变量标识符”,可用于标识符号(如函数名,函数参数及类型参数)。所以int是一个类型参数,只有Int或者INT才是类型。


以上是关于论Haskell的复用性(上篇)——多态的主要内容,如果未能解决你的问题,请参考以下文章

java三大特性性:封装继承和多态

Haskell 即席多态性

C++之STL

haskell中的多态函数列表?

java面对对象

关于Java中面向对象章节IO 流中的重点基础知识。