Objects and Polymorphism（对象与多态性）

（以下内容译自Category:Tutorials中的Objects and Polymorphism. ） 近年来，编程语言Visual Prolog有了巨大的发展，将来也还会进一步地发展. 发展最为显著的方面是语言中引入了面向对象、参数多态性等，本文要说明引入这些特性的理由. 我们会举例说明这些工具有助于解决软件复杂性及尺度的增加.

初衷
尽管Prolog有许多了不起的优点，但要看到，它毕竟是起自于信息时代的初期. 三十多年后，尤其是在IT业界突飞猛进发展的情况下，以为它会停滞在以前的那个时代是愚蠢的. 那个时候，可以写个Prolog程序描述如何让农夫带着白菜、羊和狼用一只小船过河的程序，很神奇. 现在仍可以很轻松地写出这样的程序（参见Farmer, Wolf, Goat and Cabbage），但已经没什么神奇了. 要是让孩子们来看这个程序，他们会问：怎么不是动画的？今天，在网上随便一点，就有成千上万个更好的这样的程序.

今天的程序一定要比昨天的更先进，而且必须生动、能与更广阔的外界交互. 这些都还在不断发展中.

人们进行了许多努力来解决这些挑战，给Visual Prolog添加了许多相信可以取胜的元素. 这中间就有面向对象、参数多态性，当然还有其它许多来自实用编程技术的新元素.

目标与手段
语言更新的目标，是要为创建与维护越来越复杂的程序和应对越来越复杂的环境提供更好的手段.

简化
人们对简单的东西总是要比对复杂的处理得要好，因而处理复杂事物一个重要的方法就是把它降低成简单些的事，尤其是用“结构”来组织复杂的事物.

基本的技巧就是“分而治之”：把原始的问题划分成一些可以分别解决的子问题，也就是说，全部问题的解是由若干子问题的解合成的.

可维性
对于软件来说，能解决一个问题常常是不够的，必须能通过不断地维护与扩展解决发展中的更多问题. 所以，使用的方法不仅要能解决问题，而且还要便于程序维护，这很重要. 这中间尤其重要的是程序可读性，程序的划分要清晣可见.

重用与共享
当把问题划分成子问题时，常常会看到一些相似的子问题. 重用相似的解决方案当然是件便捷的好事. 这样的解决方案可以在新的上下文关联中采用，而实现的方法有两种：可以拷贝它用在新程序中，也可以在不同问题中共用代码.

后一种显然更难些，而且只有在所有软件都还需要使用的情况下才有意义. 但也有它的好处，只维护一个内容就可以在多个地方共享使用.

灵活性
我们还希望软件很灵活. 比如，程序可能要传递一些变量给不同的用户；可能需要在情况变化时重新配置软件；还可能一些共享的东西要适应不完全相同的情况，等等.

能力
最后，我们还希望通过语言的扩充来增加编程能力. 例如，用最少的代码来处理某些标准“问题”.

多态性
Visual Prolog已经有了许多支持上述目标的特性，但要达到上述目标还是要依赖编程人员应用这些特性. 下面，我们重点来说对多态性的理解与使用.

Visual Prolog中的对象系统具有所谓 subsumption polymorphism（包容多态性），Visual Prolog 7.0 起，还具有 parametric polymorphism（参数多态性）. 多态性，非常适合于“分而治之”，还支持代码共享.

包容多态性
对象是个带有状态及代码的封装实体，它提供一个接口以便外界可以与它进行交互. 对象也可以通过接口与其它的对象交互.

源代码中，每个接口都有明确的定义，它是由一个接口名称及若干谓词声明组成的.

假设有个对象，行政系统可以用它报告员工薪酬. 为简便，对象接口可以是这样的：

interface salarySystem predicates salary : (string Name, real Amount). end interface salarySystem

带这个接口的对象，可以报告一个人（用Name表示）的薪酬数量（Amount).

salarySystem接口定义了程序中可以使用的数据类型. 应用程序中报告薪酬数量的主要谓词可以这样来声明：

class predicates reportSalary : (salarySystem SalarySystem).

作为例子，我们可以简单地用下面这样的代码：

clauses reportSalary(SalarySystem) :- SalarySystem:salary("John D", 135.12), SalarySystem:salary("Elvis P", 117.00).

当然，我们还需要实现薪酬系统的对象，就是在这儿出现了包容多态性. 事情是这样的：我们的用户当然要使用很多不同的薪酬系统，它们必须能够接受不同的输入格式，可以使用不同的介质. 对象由类来实现，而有幸的是各种各样的类可以实现相同的接口. reportSalary谓词可以使用任何一个这样的类产生的对象，如此一来，该谓词就是 polymorphic（多态的），因为salarySystem类型 subsumes （包容）了所有这个接口的不同实现. 要处理ACME薪酬系统，我们可以声明这样的类：

class acme : salarySystem end class acme

这个声明只是说：acme是个产生salarySystem类型对象的类，它当然还需要有实现：

implement acme constants fmt = "SAL:%>>>%\n". clauses salary(Name, Amount) :- stdio::writef(fmt, Name, Amount). end implement acme

为了说明多态性，我们再来实现一个O薪酬系统，再声明和实现一个类：

class oSalary : salarySystem end class oSalary implement oSalary constants fmt = "\n". clauses salary(Name, Amount) :- stdio::writef(fmt, Name, Amount). end implement oSalary

现在来综合试验这两个薪酬系统：

clauses test:- ACME = acme::new, reportSalary(ACME), Osalery = oSalary::new, reportSalary(Osalery).

用上面的测试谓词，我们创建了这两个薪酬系统并让它们各自做了报告. 结果是这样：

SAL:John D>>>135.12 SAL:Elvis P>>>117  

在这个例子中，差别并不大，但实际上我们也可以把结果放到数据库中，或是应用外部API，或是调用一个WEB服务，等等.

在上面的解决方案中应用了“分而治之”的原理，把报告工作分成了 a vendor independent part and a vendor specific part. In this case the vendor independent part also deals with the company level in the reporting, were as the vendor dependent part only deals with the person level.

通过包容多态性，我们以一种无缝的方式提供了处理多个薪酬系统的灵活性，尽管现实世界中可能没这么简单. 问题的关键在于：salarySystem接口十分有效地提取和概括了所有相关的薪酬系统. 其实，可能还需要为salary谓词提供多种标识人员的方法，让不同的薪酬系统可以使用自己相关的内容而忽略无关的内容.

reportSalary谓词是由各种薪酬系统共享的. 这种共享是一种高层级的特殊的共享，既可以在同一个应用程序中，也可以在它的变量中. （ The sharing is a high-level domain specific sharing, within the same application or variants of it.）

Visual Prolog还有另一类包容多态性：谓词的值. 谓词的值是约束给一个变量的谓词，它作为参数传递和/或保存在事实中. 多态在这里也是出现在一个类型包容许多不同的实现中.

例如，PFC在其大量使用的event notification scheme（事件通告机制）中就应用了谓词值. 这个机制也可以在PFC之外应用.

The事件通告机制有两种参与者：事件源和事件接收者. 一个事件源可以有任意个事件接收者. 事件源提供注册和注销事件接收的谓词，当事件发生时所有注册的接收者都会得到通知.

我们感兴趣的事情是：事件接收者是一个谓词，也就是通知会立即执行代码. 而且，由于包容多态性，每个接收者可以有自己的实现因而可以做出各种不同的反应.

谓词值一个非常重要的特性在于它可以是一个对象谓词，而这可以让它们访问属于自己的对象状态.

我们不在这里详细讨论事件通告机制，尽管能理解它是个好事. 读者可以自己研究PFC中的这个概念（参看 addXxxxListener）. 不过，在后面的工作计划实例中，我们会看到谓词值的使用.

参数多态性
从Visual Prolog 7.0开始，引入了参数多态性. 与包容多态性一样，参数多态性其目的是要对很多东西使用相同的代码. 不过，包容多态性主要是为相同情形提供各种实现，而参数多态性则主要是为不同情形提供相同实现.

我们从一个简单例子着手来介绍这个概念. 我们来实现一个函数，它以表为参数，逐个地返回表中的元素.

Visual Prolog中的表域是一个多形态的域：如果Elem是一个类型/域则Elem*就是Elem域的表. 新奇的是：如同Elem那样的参数和如同Elem*那样的类型表达式，可以用在声明中，而结果是声明涵盖了这样参数的所有实例.

这样，我们可以声明getMember_nd函数如下：

predicates getMember_nd : (Elem* List) -> Elem Value nondeterm.

这个声明说getMember_nd是个函数，它以Elem的表为参数，返回一个Elem，这里的Elem可以是任意域/类型的.

实现很简单：

clauses getMember_nd([V|_L]) = V.   getMember_nd([_V|L]) = getMember_nd(L).

无需进一步注明，getMember_nd可以用于任意种类的表：

clauses run:- console::init, L = 1,2,3], [7,-7], [9,4,5, foreach X = list::getMember_nd(L) do           stdio::writef("X = %\n", X)        end foreach.

这里，Elem是integer*（整数的表）. 输出是这样：

X = [1,2,3] X = [7,7] X = [9,4,5]

表域已经预定义为多态域，不过也可以自己定义多态域. 例如，我们可以创建一个优先级队列（priority queue，有时也叫堆）. 先说一下，这里的实现可能并不实用，只是个例子而已.

优先级队列的思路是：我们按优先级插入一些数据，以后再取回最低优先级的数据. 这听起来有点儿怪，为什么取回的是最低优先级的数据？不过这正是通常的情况. 可能它是按照第一优先、第二优先……，等等，这样来的吧，在这里，数字越小优先级越高.

不管怎么说，在这种情况下我们想要用tuple（元组）的表实现队列，每个元组有两个元素：第一个是优先级，第二个是数据. 还有，我们需要对表按优先级排序，总是让优先级最低的在表的最前面.

tuple 在Visual Prolog已经定义了，其定义差不多是这样的：

domains tuple{T1, T2} = tuple(T1, T2).

元组也可以是别的元维，不过这里只需要二维的，我们这里做的是程序员定义多态域的例子. 这个定义说：tuple是一个域，它有两类参数T1和T2. 可以把它想像为相应于该类型参数所有实例的无穷多个域，比如：

domains tuple_char_char = tuple(char, char). tuple_char_integer = tuple(char, integer). tuple_char_string = tuple(char, string). ...

仅前面单一一个域定义就相当于上面所有的定义，而实际上的域名称/表达式则是如下的样式：

tuple{char, char} tuple{char, integer} tuple{char, string} ...

其值与料想的一样，我们用一个稍微复杂的例子：

X = tuple(tuple('a',"a"), [1.2])

X是一个元组，它的第一个元素本身也是一个元组（含有一个字符和一个串）而第二个元素是一个实数的表. 那么，X具有的类型是：

tuple{ tuple{char, string}, real* }

可以发现，类型表达式变得相当复杂了. 可以就用它，但在下面的计划例子中我们会让表达式简单些，这样更易理解，同时也能使程序更多些类型安全性.

好了，现在来定义我们的队列域：

domains queue_rep{Priority, Data} = tuple{Priority, Data}*.

域名称中的 _rep 与上面提到的计划例子有关，在这里先可以不去管它. 这个域定义说，queue_rep是一个域，有两类参数：Priority 和 Data，而且它是这样类型元组的表.

或许会奇怪为什么Priority是一个类型参数？它不应该是个数值类型比如说，整数么？但是，在Visual Prolog 7.0中，所有数据类型都是有序的，因此任何数据类型都可以用作优先级. 在这里使用类型参数可以使队列更灵活. 最起码，我们不是非得选整数、无符号数或实数来做优先级.

要了解是否可以使用类型参数或必须要使用常规的类型，主要看你对类型的需要. 在这里我们需要的是排序（比较），而类型参数可以：


 * 作为参数传递
 * （与相同类型的值）绑定和合一
 * （与相同类型的值）比较
 * 写到流
 * 从流读出

最后一项部分上是不对的，编译器可以允许，但实际上不可能读所有类型的数据，比如不可能从流中读谓词的值和对象.

说远了，回到我们的优先级队列上来. 我们定义了表示队列的域，还需要声明和实现适当的操作.

从简单的开始吧，首先，要能取队列中最小的元素（也就是最低优先级的那个）. 可以这样声明：

predicates tryGetLeast_rep : (queue_rep{Priority, Data} Queue) -> tuple{Priority, Data} Least determ.

这个谓词是determ的，因为队列可能会是空的. 该谓词是一个函数，其参数是一个优先级队列而返回的则一个优先级及其相应数据的元组. 它不会把队列中的元素拿走，我们希望的是可以获取元素但又不移开元素.

表是按最小元素最先按序的，实现很容易：

clauses tryGetLeast_rep([Least|_]) = Least.

同样，我们还需要一个谓词能拿走最小的元素：

predicates deleteLeast_rep : (queue_rep{Priority, Data} Queue1) -> queue_rep{Priority, Data} Queue. clauses deleteLeast_rep([]) = []. deleteLeast_rep([_Least|Rest]) = Rest.

最后，还需要一个插入谓词：

predicates insert_rep : (queue_rep{Pri, Data} Q1, Pri P,  Data D) -> queue_rep{Pri, Data} Q0. clauses insert_rep([], Pri, Data) = [tuple(Pri, Data)]. insert_rep(Q1, Pri, Data) = Q0 :- [tuple(P1,D1)|Rest] = Q1, if Pri <= P1 then Q0 = [tuple(Pri, Data)| Q1] else Q0 = [tuple(P1,D1) | insert_rep(Rest, Pri, Data)] end if.

这里用了缩写的变量名以使代码尽可能在一行上写完，但同时它也表明像上面Pri那个类型的变量其范围是在它出现的声明/定义中. 因此，在一个声明里用Priority而在另一个中用Pri不会出错. 不过，当然应该慎重选择变量名，好的名称会使程序很清晰. 把Priority和Data对调一下编译器不会在乎，但肯定会把很多程序员搞晕.

Visual Prolog将来的版本中还会有类型变量，其范围是整个接口/类/实现.

上面的优先级队列可以使用在很多场合中，不过它也有两个缺点，这两个缺点都与我们定义队列域的方法有关：

domains queue_rep{Priority, Data} = tuple{Priority, Data}*.

像这样的定义，不过就是个缩写或同义词，因而 queue_rep{integer, sting} 和 tuple{integer, string}*. 是完全一样的.


 * debugger总是会使用展开的形式（也就是 tuple{integer, string}*），这可能有些乱
 * 具有这样类型的随便什么数据可能会当作参数给优先级队列谓词. 但是，优先级队列的操作不仅期望数据有这样的类型，而且还期望它按特定方式排序（即，数据是固定的）.

后面这个问题，就算没有多态性的事也常常会有. 它主要是因为对特殊的东西使用了一般的数据结构，这样做很容易把特殊的与普通的混同了.

所以，我们将queue_rep域改用为队列的内部表示，而将extension _rep对外. 对于外部世界，我们把各个queue_rep队列都封装在一个函数里.

如此一来，实际的队列域就是这样的：

domains queue{Priority, Data} = queue(queue_rep{Priority, Data}).

这样的域，既不是一个缩写也不是一个同义项了.

外露的/公用的谓词当然要操作队列，所以完整的类声明是这样的：

class priorityQueue open core domains queue_rep{Priority, Data} = tuple{Priority, Data}*. domains queue{Priority, Data} = queue(queue_rep{Priority, Data}). constants empty : queue{Priority, Data} = queue([]). predicates insert : (queue{Pri, Data} Q1, Pri Pri, Data Data) -> queue{Pri, Data} Q0. predicates tryGetLeast : (queue{Priority, Data} Queue) -> tuple{Priority, Data} Least determ. predicates deleteLeast : (queue{Priority, Data} Queue1) -> queue{Priority, Data} Queue. end class priorityQueue

在这个类里我们还加了一个空队列常数，这样，使用这个类的时候不用再考虑队列的表示问题，谓词、常数都备齐了.

有了我们以前实现的谓词，类里的这些谓词实现非常简单，它们要做的只是在适当位置上添加或删除队列函子.

clauses insert(queue(Queue), Priority, Data) = queue(insert_rep(Queue, Priority, Data)). clauses tryGetLeast(queue(Queue)) = tryGetLeast_rep(Queue). clauses deleteLeast(queue(Queue)) = queue(deleteLeast_rep(Queue)).

由于使用了附加的函子，这个_rep方案效率上稍稍差了些，而这个附加的函子用不用全由个人喜好. 但不管怎么说，它使得这里的类型是独一无二的，而不再是一个同义类型.

优先级队列可以这样用：

clauses test:- Pq1 = priorityQueue::empty, Pq2 = priorityQueue::insert(Pq1, 17, "World"), Pq3 = priorityQueue::insert(Pq2, 12, "Hello"), Pq4 = priorityQueue::insert(Pq3, 23, "!"), stdio::writef("%\n", Pq4).

输出会是这样（除了另外的空白）：

queue( [tuple(12,"Hello"), tuple(17,"World"), tuple(23,"!") ] )

多态性似乎会使类型检查受损，其实不然，它使类型检查更强而且更灵活. 例如，按下面这样写：

clauses test:- Pq1 = priorityQueue::empty, Pq2 = priorityQueue::insert(Pq1, 17, "World"), Pq3 = priorityQueue::insert(Pq2, 12, 13).

会得到错误消息：

error c504: The expression has type '::integer', which is incompatible with the type '::string'（错误c504：期望具有类型'::integer'，它与类型'::string'是不兼容的）

第一个insert强制使Pq2的Data类型是串了，所以下一行再使用整数是不行的.

可能更让人惊奇的是它也强制使Pq1的Data类型为串了，因而下面的代码也会有同样的错误：

clauses test:- Pq1 = priorityQueue::empty, Pq2 = priorityQueue::insert(Pq1, 17, "World"), Pq3 = priorityQueue::insert(Pq1, 12, 13).

另一方面，如下使用是合法的：

clauses test:- Pq1a = priorityQueue::empty, Pq2 = priorityQueue::insert(Pq1a, 17, "World"), Pq1b = priorityQueue::empty, Pq3 = priorityQueue::insert(Pq1b, 12, 13).

其实问题实质是：空常数可以是多态的，而变量则只能是单一类型. 因此，当空被绑定给变量时具体的类型就选定并“固化”了.

上面的优先级队列可以使用在许多场合，而代码则可以在多个类似的应用中共享. 如果我们把这个优先级队列做得更有效率些，所有它的应用也都能受益.

参数多态性用来以相同的方式解决不同的问题（例如优先级队列），因而它常被用来做基础库软件.

包容多态在许多方面与参数多态是互补的：它是用不同的方法解决相同的问题（例如给不同的薪酬系统提供薪酬报告），因而它常用在应用程序的高层来处理差异.

当然，很多情况并不是非黑即白, 还有很多中间层次，它们也有显著差别.

例：作业计划表
现在我们来把上面一些东西结合起来做一个例子，尽管简单但也还算强的作业计划表.

一项作业就是对要完成工作的一个估算. 我们把作业登记在计划表里，到指定的时间计划表就会启动这项作业. 这样“闹钟”式的计划表用来处理重复发生的工作及在X分钟内（而不是在某个固定时刻）启动的工作以及类似的情况是很容易的. 这里，我们只考虑这种“闹钟”功能.

计划表要依赖于定时器事件：在每次定时器事件触发时它都会查看一下是否有作业要做，有就执行.

这个例子中我们假设时间是整数，程序中我们要用若干个计划表，所以每个计划表用一个对象来表示. 这样一来，计划表主要地就可以用一个接口，即计划表对象的类型来描述. 计划表的声明是这样：

interface scheduler domains job = (integer Time) procedure (i). predicates registerJob : (integer Time, job Job). predicates timerTick : (integer Time). end interface scheduler

job（作业）就是调用一个谓词后的值，这里让job以Time调用为参数，主要是有象征意义，也比较简单. 在实际应用中，可能更好的做法是交给它计划表，而计划表则还要用一些获取当前时间及其它一些内容的谓词来充实. 这样的话，作业就可以自行决定需要获取些什么信息了.

registerJob的功用很明显.

对timerTick的调用要很有规律，它是这个计划表的核心.

实现是这样的：

implement scheduler open core, priorityQueue facts jobQueue : queue{integer, job}. clauses new :- jobQueue := empty. clauses registerJob(Time, Job) :- jobQueue := insert(jobQueue, Time, Job). clauses timerTick(Time) :- if           tuple(T, Job) = tryGetLeast(jobQueue), T <= Time then Job(Time), jobQueue := deleteLeast(jobQueue), timerTick(Time) end if. end implement scheduler

这个实现中大多是些小事，而主要的是存储作业时内部用了优先级，以时间为优先级，最接近期限的作业最易访问.

jobQueue事实是在构造器中初始化的，而registerJob只是把作业插入到优先级队列中.

重要的事情都是在timerTick中开展的，它取出队列中“最小的”作业，如果这个作业现在就应该启动或早就该启动了，就启动它并把它从队列中删除. 接着，再考虑队列中其余的项.

If如果“最小的”作业并没有到期，则什么也不做，再等下一次定时器触发.

Before在开始使用这个计划表之前，要说一下为什么这中间不包含实际的定时器，这有几个方面的好处：


 * 与某个外部时钟保持同步可能是很重要的
 * 在某些关联中Time参数的增量在所有时间上不一定都要一样
 * 在有些关联中Time未必就是真实的时间（例如，是游戏中的一步，工作流程中的一步，等等）.
 * 在GUI应用程序中，可能需要timerTick是从一个窗口事件中调用的，也就是说要保持应用程序为单线程.

使用外在的定时器触发能满足上述所有要求.

我们用个控制台程序的定时器，这个定时器就是一个循环：

class predicates traceJob : (integer Time). clauses traceJob(Time) :- stdio::write("Now: ", Time, "\n"). clauses test :- Scheduler = scheduler::new, Scheduler:registerJob(500, traceJob), foreach Time = std::fromTo(1,1000) do           Scheduler:timerTick(Time) end foreach.

traceJob只是简单地写时间. 先创建计划表，然后在Time = 500时登记traceJob，接着启动时钟.

我们也可以搞一个能重定自身时间的作业，这样它就可以反复地运行. 要这样做的话需要能够访问计划表，为此我们可以把计划表保存在一个事实里：

class predicates cyclicJob : (integer Time). clauses cyclicJob(Time) :- stdio::write("Hello World!\n"), scheduler:registerJob(Time+50, cyclicJob). class facts scheduler : scheduler := erroneous. clauses test :- scheduler := scheduler::new, scheduler:registerJob(5, cyclicJob), foreach Time = std::fromTo(1,200) do           scheduler:timerTick(Time) end foreach.

在计划表里，我们清楚地使用了参数多态性的优先级队列. 我们还在谓词值里使用了包容多态性，以便在计划表里用不同的实现登记作业. 参数多态性已经充分准备好了事物优先顺序方面的同类处理工作，不用再考虑处理的是什么事物. 而包容多态性则用来处理作业本身不同的特性.

其它新结构
本文只讨论了一部分语言上的新特点，但也应用了新的 foreach 和 if-then-else 结构而并没有特别介绍它们，因为它们很好理解，不需要多说. 不过，读者可能已经注意到有时它们的结构有些奇怪，如 "catch all" 子句及多余的截断等. 尽管它们不像是“老的”Prolog，但实际上具有很显著的Prolog风格的语义，同时又使得代码很清晰.

总结
我们确信，参数多态性有助于不同问题共享解决方案，可以减少开发时间，尤其是节约维护时间. 这也可以让我们创建更多的软件库来使用现成的东西，实际上是共享现成的东西.

我们也确信，包容多态性有助于软件中的同类事物，不管是内部层级的如计划表例子的情况，还是外部层级的如薪酬报告例子的情况，都能有帮助.

展望
Visual Prolog还会要发展，还有许多新特点已在计划中并在逐步实现，有两点特别值得指出来：

参数多态性将要扩展到覆盖类与接口. 这将增强我们前面讨论过的那些能力，自然还会要使所有数据类型都有这样的能力，而不只局限在一个子集上. 还有，参数多态性将成为F-bounded，当然这超出了本文讨论的范围，有兴趣的读者可以查看相关文献，也可以在互联网上搜索.

另一个有意思的匿名谓词值. 它可以极大地简化代码，没有它就必须要写得到某个效果的代码，比如，在上面讨论的作业计划表中创建作业的情况. 给这样的作业传递参数总是有点麻烦的，作业本身并不取用任何用户定义的参数，所以参数必须要放在什么地方. 一般是要放在属于作业谓词的对象事实中. 用了匿名谓词，这就没必要了.