just copying http://www.comlab.ox.ac.uk/people/ralf.hinze/talks/Open.pdf:
"Open recursion Another handy feature offered by most languages with objects and classes is the ability for one method body to invoke another method of the same object via a special variable called self or, in some langauges, this. The special behavior of self is that it is late-bound, allowing a method defined in one class to invoke another method that is defined later, in some subclass of the first. "
This paper analyzes the possibility of adding OO to ML, with regards to expressivity and complexity. It has the following excerpt on objects, which seems to make this term relatively clear –
3.3. Objects
The simplest form of object is just a record of functions that share a common closure environment that
carries the object state (we can call these simple objects). The function members of the record may or may not
be defined as mutually recursive. However, if one wants to support inheritance with overriding, the structure
of objects becomes more complicated. To enable open recursion, the call-graph of the method functions
cannot be hard-wired, but needs to be implemented indirectly, via object self-reference. Object self-reference
can be achieved either by construction, making each object a recursive, self-referential value (the fixed-point
model), or dynamically, by passing the object as an extra argument on each method call (the self-application
or self-passing model).5 In either case, we will call these self-referential objects.
The name "open recursion" is a bit misleading at first, because it has nothing to do with the recursion that normally is used (a function calling itself); and to that extent, there is no closed recursion.
It basically means, that a thing is referring to itself. I can only guess, but I do think that the term "open" comes from open as in "open for extension".
In that sense an object is open to extension, but still referring to itself.
Perhaps a small example can shed some light on the concept.
Imaging you write a Python class like this one:
class SuperClass:
def method1(self):
self.method2()
def method2(self):
print(self.__class__.__name__)
If you ran this by
s = SuperClass()
s.method1()
It will print "SuperClass".
Now we create a subclass from SuperClass and override method2:
class SubClass(SuperClass):
def method2(self):
print(self.__class__.__name__)
and run it:
sub = SubClass()
sub.method1()
Now "SubClass" will be printed.
Still, we only call method1() as before. Inside method1() the method2() is called, but both are bound to the same reference (self in Python, this in Java). During sub-classing SuperClass method2() is changed, which means that an object of SubClass refers to a different version of this method.
That is open recursion.
In most cases, you override methods and call the overridden methods directly.
This scheme here is using an indirection over self-reference.
P.S.: I don't think this has been invented but discovered and then explained.
在 C++ 中,名称解析都是静态的,通常是名称查找。不同范围的名称查找规则有所不同。它们中的大多数与C中的标识符查找规则一致(除了C中允许隐式声明但C++中不允许):必须首先声明名称,然后才能在源代码中(词法上)查找该名称稍后,否则程序格式错误(需要在语言的实现中发出错误)。这种名称依赖性的严格要求相当“封闭”,因为以后没有机会从错误中恢复,因此您不能直接在不同的声明中相互引用名称。
此外,根据 TAPL 中的描述,C++ 确实具有“开放递归”:this 指针和virtual 函数。确定虚拟功能的目标(覆盖者)的规则独立于名称查找规则。派生类中定义的非静态成员通常只是隐藏基类中同名的实体。调度规则仅在虚拟函数调用上、在名称查找之后生效(由于 C++ 函数调用的评估是严格,因此顺序得到保证,或应用)。通过 using 声明也可以轻松引入基类名称,而无需担心实体的类型。
OTOH,Java 有一些更复杂的规则来混合名称查找和其他规则,包括如何识别覆盖者。 Java 子类中的名称隐藏特定于实体类型。区分不同类型的覆盖和重载/遮蔽/隐藏/模糊更加复杂。在子类的定义中也不可能存在 C++ 的 using 声明技术。无论如何,这种复杂性并不会让 Java 比 C++ 更“OOP”。
In short, open recursion is about something actually not related to OOP, but more general.
The relation with OOP comes from the fact that many typical "OOP" PLs have such properties, but it is essentially not tied to any distinguishing features about OOP.
So there are different meanings, even in same "OOP" language. I will illustrate it later.
Etymology
As mentioned here, the terminology is likely coined in the famous TAPL by BCP, which illustrates the meaning by concrete OOP languages.
TAPL does not define "open recursion" formally. Instead, it points out the "special behavior of self (or this) is that it is late-bound, allowing a method defined in one class to invoke another method that is defined later, in some subclass of the first".
Nevertheless, neither of "open" and "recursion" comes from the OOP basis of a language. (Actually, it is also nothing to do with static types.) So the interpretation (or the informal definition, if any) in that source is overspecified in nature.
Ambiguity
The mentioning in TAPL clearly shows "recursion" is about "method invocation". However, it is not that simple in real languages, which usually do not have primitive semantic rules on the recursive invocation itself. Real languages (including the ones considered as OOP languages) usually specify the semantics of such invocation for the notation of the method calls. As syntactic devices, such calls are subject to the evaluation of some kind of expressions relying on the evaluations of its subexpressions. These evaluations imply the resolution of method name, under some independent rules. Specifically, such rules are about name resolution, i.e. to determine the denotation of a name (typically, a symbol, an identifier, or some "qualified" name expressions) in the subexpression. Name resolution often respects to scoping rules.
OTOH, the "late-bound" property emphasizes how to find the target implementation of the named method. This is a shortcut of evaluation of specific call expressions, but it is not general enough, because entities other than methods can also have such "special" behavior, even make such behavior not special at all.
A notable ambiguity comes from such insufficient treatment. That is, what does a "binding" mean. Traditionally, a binding can be modeled as a pair of a (scoped) name and its bound value, i.e. a variable binding. In the special treatment of "late-bound" ones, the set of allowed entities are smaller: methods instead of all named entities. Besides the considerably undermining the abstraction power of the language rules at meta level (in the language specification), it does not cease the necessity of traditional meaning of a binding (because there are other non-method entities), hence confusing. The use of a "late-bound" is at least an instance of bad naming. Instead of "binding", a more proper name would be "dispatching".
Worse, the use in TAPL directly mix the two meanings when dealing with "recusion". The "recursion" behavior is all about finding the entity denoted by some name, not just specific to method invocation (even in those OOP language).
The title of the chapter (Case Study: Imperative Objects) also suggests some inconsistency. Obviously, the so-called late binding of method invocation has nothing to do with imperative states, because the resolution of the dispatching does not require mutable metadata of invocation. (In some popular sense of implementation, the virtual method table need not to be modifiable.)
Openness
The use of "open" here looks like mimic to open (lambda) terms. An open term has some names not bound yet, so the reduction of such a term must do some name resolution (to compute the value of the expression), or the term is not normalized (never terminate in evaluation). There is no difference between "late" or "early" for the original calculi because they are pure, and they have the Church-Rosser property, so whether "late" or not does not alter the result (if it is normalized).
This is not the same in the language with potentially different paths of dispatching. Even that the implicit evaluation implied by the dispatching itself is pure, it is sensitive to the order among other evaluations with side effects which may have dependency on the concrete invocation target (for example, one overrider may mutate some global state while another can not). Of course in a strictly pure language there can be no observable differences even for any radically different invocation targets, a language rules all of them out is just useless.
Then there is another problem: why it is OOP-specific (as in TAPL)? Given that the openness is qualifying "binding" instead of "dispatching of method invocation", there are certainly other means to get the openness.
One notable instance is the evaluation of a procedure body in traditional Lisp dialects. There can be unbound symbols in the body and they are only resolved when the procedure being called (rather than being defined). Since Lisps are significant in PL history and the are close to lambda calculi, attributing "open" specifically to OOP languages (instead of Lisps) is more strange from the PL tradition. (This is also a case of "making them not special at all" mentioned above: every names in function bodies are just "open" by default.)
It is also arguable that the OOP style of self/this parameter is equivalent to the result of some closure conversion from the (implicit) environment in the procedure. It is questionable to treat such features primitive in the language semantics.
(It may be also worth noting, the special treatment of function calls from symbol resolution in other expressions is pioneered by Lisp-2 dialects, not any of typical OOP languages.)
More cases
As mentioned above, different meanings of "open recursion" may coexist in a same "OOP" language.
C++ is the first instance here, because there are sufficient reasons to make them coexist.
In C++, name resolution are all static, normatively name lookup. The rules of name lookup vary upon different scopes. Most of them are consistent with identifier lookup rules in C (except for the allowance of implicit declarations in C but not in C++): you must first declare the name, then the name can be lookup in the source code (lexically) later, otherwise the program is ill-formed (and it is required to issue an error in the implementation of the language). The strict requirement of such dependency of names are considerable "closed", because there are no later chance to recover from the error, so you cannot directly have names mutually referenced across different declarations.
To work around the limitation, there can be some additional declarations whose sole duty is to break the cyclic dependency. Such declarations are called "forward" declarations. Using of forward declarations still does not require "open" recursion, because every well-formed use must statically see the previous declaration of that name, so each name lookup does not require additional "late" binding.
However, C++ classes have special name lookup rules: some entities in the class scope can be referred in the context prior to their declaration. This makes mutual recursive use of name across different declarations possible without any additional "forward" declarations to break the cycle. This is exactly the "open recursion" in TAPL sense except that it is not about method invocation.
Moreover, C++ does have "open recursion" as per the descriptions in TAPL: this pointer and virtual functions. Rules to determine the target (overrider) of virtual functions are independent to the name lookup rules. A non-static member defined in a derived class generally just hide the entities with same name in the base classes. The dispatching rules kick in only on virtual function calls, after the name lookup (the order is guaranteed since evaulations of C++ function calls are strict, or applicative). It is also easy to introduce a base class name by using-declaration without worry about the type of the entity.
Such design can be seen as an instance of separate of concerns. The name lookup rules allows some generic static analysis in the language implementation without special treatment of function calls.
OTOH, Java have some more complex rules to mix up name lookup and other rules, including how to identify the overriders. Name shadowing in Java subclasses is specific to the kind of entities. It is more complicate to distinguish overriding with overloading/shadowing/hiding/obscuring for different kinds. There also cannot be techniques of C++'s using-declarations in the definition of subclasses. Such complexity does not make Java more or less "OOP" than C++, anyway.
Other consequences
Collapsing the bindings about name resolution and dispatching of method invocation leads to not only ambiguity, complexity and confusion, but also more difficulties on the meta level. Here meta means the fact that name binding can exposing properties not only available in the source language semantics, but also subject to the meta languages: either the formal semantic of the language or its implementation (say, the code to implement an interpreter or a compiler).
For example, as in traditional Lisps, binding-time can be distinguished from evaluation-time, because program properties revealed in binding-time (value binding in the immediate contexts) is more close to meta properties compared to evaluation-time properties (like the concrete value of arbitrary objects). An optimizing compiler can deploy the code generation immediately depending on the binding-time analysis either statically at the compile-time (when the body is to be evaluate more than once) or derferred at runtime (when the compilation is too expensive). There is no such option for languages blindly assume all resolutions in closed recursion faster than open ones (and even making them syntactically different at the very first). In such sense, OOP-specific open recursion is not just not handy as advertised in TAPL, but a premature optimization: giving up metacompilation too early, not in the language implementation, but in the language design.
发布评论
评论(5)
只是复制 http://www.comlab.ox.ac .uk/people/ralf.hinze/talks/Open.pdf:
“开放递归大多数带有对象和类的语言提供的另一个方便的功能是一个方法体能够通过一个称为 self 的特殊变量调用同一对象的另一个方法,或者在某些语言中称为 this。 self 的特殊行为是它是后期绑定的,允许一个类中定义的方法调用稍后在第一个类的某个子类中定义的另一个方法。”
just copying http://www.comlab.ox.ac.uk/people/ralf.hinze/talks/Open.pdf:
"Open recursion Another handy feature offered by most languages with objects and classes is the ability for one method body to invoke another method of the same object via a special variable called self or, in some langauges, this. The special behavior of self is that it is late-bound, allowing a method defined in one class to invoke another method that is defined later, in some subclass of the first. "
这篇论文分析了添加的可能性OO 到 ML,关于表达性和复杂性。它有以下关于物体的摘录,这似乎使这个术语相对清晰——
This paper analyzes the possibility of adding OO to ML, with regards to expressivity and complexity. It has the following excerpt on objects, which seems to make this term relatively clear –
“开放递归”这个名字一开始有点误导,因为它与通常使用的递归(函数调用自身)无关;从这个意义上说,不存在封闭递归。
它基本上意味着,一个事物指的是它自己。我只能猜测,但我确实认为“开放”一词来自开放,如“开放扩展”。
从这个意义上说,一个对象可以扩展,但仍然引用它自己。
也许一个小例子可以阐明这个概念。
想象一下,您编写了一个像这样的 Python 类:
如果您运行它,
它将打印“SuperClass”。
现在我们从 SuperClass 创建一个子类并覆盖 method2:
并运行它:
现在将打印“SubClass”。
尽管如此,我们仍然像以前一样只调用method1()。在 method1() 内部,method2() 被调用,但两者都绑定到相同的引用(Python 中的 self,Java 中的 this)。在子类化期间,SuperClass method2() 发生了变化,这意味着 SubClass 的对象引用了该方法的不同版本。
这就是开放递归。
在大多数情况下,您可以重写方法并直接调用重写的方法。
这里的方案使用了自引用的间接寻址。
PS:我认为这不是发明出来的,而是被发现然后解释的。
The name "open recursion" is a bit misleading at first, because it has nothing to do with the recursion that normally is used (a function calling itself); and to that extent, there is no closed recursion.
It basically means, that a thing is referring to itself. I can only guess, but I do think that the term "open" comes from open as in "open for extension".
In that sense an object is open to extension, but still referring to itself.
Perhaps a small example can shed some light on the concept.
Imaging you write a Python class like this one:
If you ran this by
It will print "SuperClass".
Now we create a subclass from SuperClass and override method2:
and run it:
Now "SubClass" will be printed.
Still, we only call method1() as before. Inside method1() the method2() is called, but both are bound to the same reference (self in Python, this in Java). During sub-classing SuperClass method2() is changed, which means that an object of SubClass refers to a different version of this method.
That is open recursion.
In most cases, you override methods and call the overridden methods directly.
This scheme here is using an indirection over self-reference.
P.S.: I don't think this has been invented but discovered and then explained.
简而言之,开放递归实际上与 OOP 无关,但更通用。
与 OOP 的关系来自于这样一个事实:许多典型的“OOP”PL 都具有此类属性,但它本质上与 OOP 的任何显着特征无关。
因此,即使在相同的“OOP”语言中,也有不同的含义。稍后我会举例说明。
词源
正如此处所述,术语可能是是著名的 TAPL by BCP 创造的,用具体的 OOP 语言说明了其含义。
TAPL 没有正式定义“开放递归”。相反,它指出“
self
(或this
)的特殊行为是它是后期绑定,允许在一个方法中定义一个方法。类来调用稍后在第一个子类中定义的另一个方法”。然而,“开放”和“递归”都不是来自语言的 OOP 基础。 (实际上,它也与静态类型无关。)因此该源中的解释(或非正式定义,如果有的话)本质上是过度指定的。
歧义
TAPL 中的提及清楚地表明“递归”是关于“方法调用”。然而,在现实语言中,情况并非如此简单,递归调用本身通常没有原始语义规则。真实语言(包括被视为 OOP 语言的语言)通常为方法调用的表示法指定此类调用的语义。作为句法设备,此类调用受到依赖于其子表达式的评估的某种表达式的评估的影响。这些评估意味着在一些独立规则下对方法名称的解析。具体来说,此类规则与名称解析有关,即确定名称的表示(通常是符号、标识符或某些“子表达式中的“限定”名称表达式)。名称解析通常遵循范围规则。
OTOH,“后期绑定”属性强调如何找到命名方法的目标实现。这是对特定调用表达式求值的捷径,但不够通用,因为方法以外的实体也可以有这种“特殊”行为,甚至使这种行为根本不特殊。
这种不充分的治疗产生了明显的歧义。也就是说,“绑定”是什么意思。传统上,绑定可以建模为一对(作用域)名称及其绑定值,即变量绑定。在“后期绑定”实体的特殊处理中,允许的实体集较小:方法而不是所有命名实体。除了在元级别(在语言规范中)大大削弱语言规则的抽象能力之外,它并没有消除绑定的传统含义的必要性(因为还有其他非方法实体),因此令人困惑。使用“后期绑定”至少是一个不好的命名实例。更合适的名称应该是“调度”,而不是“绑定”。
更糟糕的是,在处理“回避”时,TAPL 中的使用直接混合了两种含义。 “递归”行为都是关于查找由某个名称表示的实体,而不仅仅是特定于方法调用(即使在那些 OOP 语言中)。
本章的标题(案例研究:命令式对象)也表明存在一些不一致之处。显然,所谓的方法调用的后期绑定与命令式状态无关,因为分派的解析不需要调用的可变元数据。 (在某些流行的实现意义上,虚拟方法表不需要是可修改的。)
开放性
这里“开放”的使用看起来像是对开放 (lambda) 术语的模仿。开放项有一些尚未绑定的名称,因此此类项的约简必须执行一些名称解析(以计算表达式的值),否则该项不会标准化(永远不会在求值中终止)。对于原始演算来说,“晚”或“早”没有区别,因为它们是纯粹的,并且它们具有 Church-Rosser 性质,因此无论“晚”与否都不会改变结果(如果已标准化)。
这在具有可能不同的调度路径的语言中是不同的。即使分派本身隐含的隐式求值是纯粹的,它对其他求值之间的顺序也很敏感,并且具有副作用,这些副作用可能依赖于具体的调用目标(例如,一个重写器可能会改变某些全局状态,而另一个重写器则不能) 。当然,在严格的纯语言中,即使对于任何完全不同的调用目标,也不会存在可观察到的差异,排除所有这些的语言是没有用的。
那么还有另一个问题:为什么它是 OOP 特定的(如在 TAPL 中)?鉴于开放性是限定“绑定”而不是“方法调用的分派”,当然还有其他方法来获得开放性。
一个值得注意的例子是用传统 Lisp 方言对过程体进行求值。主体中可以存在未绑定的符号,并且仅在调用过程(而不是定义)时才解析它们。由于 Lisps 在 PL 历史上具有重要意义,并且与 lambda 演算很接近,因此将“开放”专门归因于 OOP 语言(而不是 Lisps)与 PL 传统相比更加奇怪。 (这也是上面提到的“让它们一点也不特殊”的情况:函数体中的每个名称默认都是“开放”的。)
还有一点值得商榷,
self
/ 的 OOP 风格this
参数相当于过程中(隐式)环境 的某些闭包转换的结果。在语言语义中将这些特征视为原始特征是值得怀疑的。(可能还值得注意的是,对其他表达式中符号解析的函数调用的特殊处理是由 Lisp 首创的-2种方言,不是任何典型的OOP语言。)
更多情况
如上所述,“开放递归”的不同含义可以共存于同一种“OOP”语言中。
C++是这里的第一个例子,因为有足够的理由让它们共存。
在 C++ 中,名称解析都是静态的,通常是名称查找。不同范围的名称查找规则有所不同。它们中的大多数与C中的标识符查找规则一致(除了C中允许隐式声明但C++中不允许):必须首先声明名称,然后才能在源代码中(词法上)查找该名称稍后,否则程序格式错误(需要在语言的实现中发出错误)。这种名称依赖性的严格要求相当“封闭”,因为以后没有机会从错误中恢复,因此您不能直接在不同的声明中相互引用名称。
为了解决这个限制,可以有一些额外的声明,其唯一的职责是打破循环依赖。此类声明称为“前向”声明。使用前向声明仍然不需要“开放”递归,因为每个格式正确的使用都必须静态地查看该名称的先前声明,因此每个名称查找不需要额外的“后期”绑定。
但是,C++ 类具有特殊的名称查找规则:类范围内的某些实体可以在声明之前在上下文中引用。这使得跨不同声明的名称的相互递归使用成为可能,而无需任何额外的“前向”声明来打破循环。这正是 TAPL 意义上的“开放递归”,只不过它与方法调用无关。
此外,根据 TAPL 中的描述,C++ 确实具有“开放递归”:
this
指针和virtual
函数。确定虚拟功能的目标(覆盖者)的规则独立于名称查找规则。派生类中定义的非静态成员通常只是隐藏基类中同名的实体。调度规则仅在虚拟函数调用上、在名称查找之后生效(由于 C++ 函数调用的评估是严格,因此顺序得到保证,或应用)。通过using
声明也可以轻松引入基类名称,而无需担心实体的类型。这种设计可以被视为关注点分离的一个实例。名称查找规则允许在语言实现中进行一些通用静态分析,而无需对函数调用进行特殊处理。
OTOH,Java 有一些更复杂的规则来混合名称查找和其他规则,包括如何识别覆盖者。 Java 子类中的名称隐藏特定于实体类型。区分不同类型的覆盖和重载/遮蔽/隐藏/模糊更加复杂。在子类的定义中也不可能存在 C++ 的
using
声明技术。无论如何,这种复杂性并不会让 Java 比 C++ 更“OOP”。其他后果
打破名称解析和方法调用分派的绑定不仅会导致歧义、复杂性和混乱,还会在元级别上带来更多困难。这里元意味着名称绑定不仅可以公开源语言语义中可用的属性,而且还受元语言的约束:语言的形式语义或其实现(例如,实现解释器或编译器的代码) )。
例如,与传统 Lisp 一样,绑定时间可以与评估时间区分开来,因为在绑定时间(直接上下文中的值绑定)中显示的程序属性是与评估时属性(如任意对象的具体值)相比,更接近元属性。优化编译器可以根据绑定时分析立即部署代码生成,可以在编译时静态(当主体要多次求值时)或在运行时推迟(当编译成本太高时)。对于语言来说,没有这样的选择,盲目地假设封闭递归中的所有解析比开放递归更快(甚至一开始就使它们在语法上不同)。从这个意义上说,特定于 OOP 的开放递归不仅不像 TAPL 中宣传的那样方便,而且是一种过早的优化:放弃元编译太早了,不是在语言实现上,而是在语言设计上。
In short, open recursion is about something actually not related to OOP, but more general.
The relation with OOP comes from the fact that many typical "OOP" PLs have such properties, but it is essentially not tied to any distinguishing features about OOP.
So there are different meanings, even in same "OOP" language. I will illustrate it later.
Etymology
As mentioned here, the terminology is likely coined in the famous TAPL by BCP, which illustrates the meaning by concrete OOP languages.
TAPL does not define "open recursion" formally. Instead, it points out the "special behavior of
self
(orthis
) is that it is late-bound, allowing a method defined in one class to invoke another method that is defined later, in some subclass of the first".Nevertheless, neither of "open" and "recursion" comes from the OOP basis of a language. (Actually, it is also nothing to do with static types.) So the interpretation (or the informal definition, if any) in that source is overspecified in nature.
Ambiguity
The mentioning in TAPL clearly shows "recursion" is about "method invocation". However, it is not that simple in real languages, which usually do not have primitive semantic rules on the recursive invocation itself. Real languages (including the ones considered as OOP languages) usually specify the semantics of such invocation for the notation of the method calls. As syntactic devices, such calls are subject to the evaluation of some kind of expressions relying on the evaluations of its subexpressions. These evaluations imply the resolution of method name, under some independent rules. Specifically, such rules are about name resolution, i.e. to determine the denotation of a name (typically, a symbol, an identifier, or some "qualified" name expressions) in the subexpression. Name resolution often respects to scoping rules.
OTOH, the "late-bound" property emphasizes how to find the target implementation of the named method. This is a shortcut of evaluation of specific call expressions, but it is not general enough, because entities other than methods can also have such "special" behavior, even make such behavior not special at all.
A notable ambiguity comes from such insufficient treatment. That is, what does a "binding" mean. Traditionally, a binding can be modeled as a pair of a (scoped) name and its bound value, i.e. a variable binding. In the special treatment of "late-bound" ones, the set of allowed entities are smaller: methods instead of all named entities. Besides the considerably undermining the abstraction power of the language rules at meta level (in the language specification), it does not cease the necessity of traditional meaning of a binding (because there are other non-method entities), hence confusing. The use of a "late-bound" is at least an instance of bad naming. Instead of "binding", a more proper name would be "dispatching".
Worse, the use in TAPL directly mix the two meanings when dealing with "recusion". The "recursion" behavior is all about finding the entity denoted by some name, not just specific to method invocation (even in those OOP language).
The title of the chapter (Case Study: Imperative Objects) also suggests some inconsistency. Obviously, the so-called late binding of method invocation has nothing to do with imperative states, because the resolution of the dispatching does not require mutable metadata of invocation. (In some popular sense of implementation, the virtual method table need not to be modifiable.)
Openness
The use of "open" here looks like mimic to open (lambda) terms. An open term has some names not bound yet, so the reduction of such a term must do some name resolution (to compute the value of the expression), or the term is not normalized (never terminate in evaluation). There is no difference between "late" or "early" for the original calculi because they are pure, and they have the Church-Rosser property, so whether "late" or not does not alter the result (if it is normalized).
This is not the same in the language with potentially different paths of dispatching. Even that the implicit evaluation implied by the dispatching itself is pure, it is sensitive to the order among other evaluations with side effects which may have dependency on the concrete invocation target (for example, one overrider may mutate some global state while another can not). Of course in a strictly pure language there can be no observable differences even for any radically different invocation targets, a language rules all of them out is just useless.
Then there is another problem: why it is OOP-specific (as in TAPL)? Given that the openness is qualifying "binding" instead of "dispatching of method invocation", there are certainly other means to get the openness.
One notable instance is the evaluation of a procedure body in traditional Lisp dialects. There can be unbound symbols in the body and they are only resolved when the procedure being called (rather than being defined). Since Lisps are significant in PL history and the are close to lambda calculi, attributing "open" specifically to OOP languages (instead of Lisps) is more strange from the PL tradition. (This is also a case of "making them not special at all" mentioned above: every names in function bodies are just "open" by default.)
It is also arguable that the OOP style of
self
/this
parameter is equivalent to the result of some closure conversion from the (implicit) environment in the procedure. It is questionable to treat such features primitive in the language semantics.(It may be also worth noting, the special treatment of function calls from symbol resolution in other expressions is pioneered by Lisp-2 dialects, not any of typical OOP languages.)
More cases
As mentioned above, different meanings of "open recursion" may coexist in a same "OOP" language.
C++ is the first instance here, because there are sufficient reasons to make them coexist.
In C++, name resolution are all static, normatively name lookup. The rules of name lookup vary upon different scopes. Most of them are consistent with identifier lookup rules in C (except for the allowance of implicit declarations in C but not in C++): you must first declare the name, then the name can be lookup in the source code (lexically) later, otherwise the program is ill-formed (and it is required to issue an error in the implementation of the language). The strict requirement of such dependency of names are considerable "closed", because there are no later chance to recover from the error, so you cannot directly have names mutually referenced across different declarations.
To work around the limitation, there can be some additional declarations whose sole duty is to break the cyclic dependency. Such declarations are called "forward" declarations. Using of forward declarations still does not require "open" recursion, because every well-formed use must statically see the previous declaration of that name, so each name lookup does not require additional "late" binding.
However, C++ classes have special name lookup rules: some entities in the class scope can be referred in the context prior to their declaration. This makes mutual recursive use of name across different declarations possible without any additional "forward" declarations to break the cycle. This is exactly the "open recursion" in TAPL sense except that it is not about method invocation.
Moreover, C++ does have "open recursion" as per the descriptions in TAPL:
this
pointer andvirtual
functions. Rules to determine the target (overrider) of virtual functions are independent to the name lookup rules. A non-static member defined in a derived class generally just hide the entities with same name in the base classes. The dispatching rules kick in only on virtual function calls, after the name lookup (the order is guaranteed since evaulations of C++ function calls are strict, or applicative). It is also easy to introduce a base class name byusing
-declaration without worry about the type of the entity.Such design can be seen as an instance of separate of concerns. The name lookup rules allows some generic static analysis in the language implementation without special treatment of function calls.
OTOH, Java have some more complex rules to mix up name lookup and other rules, including how to identify the overriders. Name shadowing in Java subclasses is specific to the kind of entities. It is more complicate to distinguish overriding with overloading/shadowing/hiding/obscuring for different kinds. There also cannot be techniques of C++'s
using
-declarations in the definition of subclasses. Such complexity does not make Java more or less "OOP" than C++, anyway.Other consequences
Collapsing the bindings about name resolution and dispatching of method invocation leads to not only ambiguity, complexity and confusion, but also more difficulties on the meta level. Here meta means the fact that name binding can exposing properties not only available in the source language semantics, but also subject to the meta languages: either the formal semantic of the language or its implementation (say, the code to implement an interpreter or a compiler).
For example, as in traditional Lisps, binding-time can be distinguished from evaluation-time, because program properties revealed in binding-time (value binding in the immediate contexts) is more close to meta properties compared to evaluation-time properties (like the concrete value of arbitrary objects). An optimizing compiler can deploy the code generation immediately depending on the binding-time analysis either statically at the compile-time (when the body is to be evaluate more than once) or derferred at runtime (when the compilation is too expensive). There is no such option for languages blindly assume all resolutions in closed recursion faster than open ones (and even making them syntactically different at the very first). In such sense, OOP-specific open recursion is not just not handy as advertised in TAPL, but a premature optimization: giving up metacompilation too early, not in the language implementation, but in the language design.
开放递归允许通过特殊变量(如 this 或 self)从内部调用对象的其他方法。
Open recursion allows to call another methods of object from within, through special variable like this or self.