It did not come as a surprise that either translation was possible at all but how about the results of comparing the complexity of performing one calculus in the other? Table 3.5 on the next page summarizes the results for emulating OPULUS in
theλ-calculus and table 3.6 on the following page shows the results for emulatingλ- calculus in OPULUS. Ranges of complexity denote the best and worst case, whereas
the average case is always closer to the lower bound.
Let us first regard the standard translation which we did not explicate in the preceding sections but that we remember as using a straightforward term repre- sentation with associated reduction functions. We note that the derivation of emu- lating theλ-calculus in OPULUSwas done within two pages [Thierbach96] resulting
in three conceptually easy object definitions all implementing two messages each (substitution and β-reduction). Deriving the opposite standard translation took more than three pages and resulted in nine translation rules needing nine construc- tors with nine associated selection functions. In addition, this does not include the emulation of OPULUS names in theλ-calculus.
The difference in verbosity of the two emulations can be explained by two rea- sons:
1. The λ-calculus is somewhat weaker in its basic operations causing the need for a name comparison emulation and
2. OPULUS is a somewhat bigger calculus with a larger grammar causing the need for more (nine compared to three) emulation rules.
On the one hand the size of OPULUS may appear arbitrary given the current ma-
turity of object-oriented calculi and one may easily excuse the λ-calculus for its efforts to emulate such a “clumsy” formalism. On the other hand, OPULUS does
a very good job of capturing the basic mechanisms of class based object-oriented languages. Capturing the others calculus’ richness is part of the game and the λ- calculus has obviously a harder job.
The first of the above argument also shows up in the complexity comparison: One full reduction step in OPULUSneedsO(nv)steps in theλ-calculus (see table 3.5), whereasvis the number of distinct names in the OPULUS term.
Fast translation Standard translation Substitution O(1) O(n)
Application O(nv) O(nv) Redex contraction O(nv) O(nv) Redex recognition O(1) O(1)–O(nv) Reduction-step O(nv) O(nv)–O(n2v)†
† It easy to reduce this to
O(nv)by combining the functions that search a redex and perform a reduction.
Table 3.5: Complexity of Opulus in theλ-calculus
Fast translation Standard translation Substitution O(n) O(n)
Redex contraction O(n) O(n) Redex recognition O(1) O(1)–O(n) Reduction-step O(n) O(n)
Table 3.6: Complexity ofλ-calculus in Opulus
It is interesting to note that the built-in name comparison of OPULUS causes
extra work when emulated in the λ-calculus. Yet, it should not be given to much importance, since the λ-calculus can easily be extended withδ-reductions that ac- complish the required comparisons withO(1)complexity.
So, abstracting from the name evaluation overhead the two standard transla- tions essentially do not differ (compare table 3.5 and table 3.6). Let us turn to the fast translations: It immediately strikes our attention that redex recognition isO(1) in either translation. This is due to the fact that redexes are directly translated into
redexes of the other calculus. Besides this exception, there is the usual overhead for name comparisons in theλ-calculus. Notwithstanding, substitution in OPULUS (re- placing$and#) is justO(1)in theλ-calculus. This is achieved by usingβ-reduction, i.e., the application of λsel f.λarg.A to the values of $and #, reduces in two (constant) steps.
Substitution ofλ-calculus terms in OPULUS has linear complexity, because vari-
ables have to be looked up in the variable environment. In an object-oriented pro- gramming language this step is constant too, since it simply requires an attribute access or — in the worst case — an access through an indirection table in case of an overridden attribute.
So, on the one hand substitutingλ-calculus terms takes linear time in OPULUS
but on the other hand note that looking up OPULUS methods takes linear time in
theλ-calculus (see row “Application” in table 3.5 on the facing page). There is no equivalent to looking up method names in the λ-calculus which is why there is no such entry in table 3.6 on the preceding page. De facto, these prominent dif- ference in complexity of emulations point out a crucial difference of the underly- ing paradigms: Object-orientation is characterized by the atomicity of looking up a name and performing the corresponding method. Functional programming is characterized by having access to function parameters at any place in any nesting depth.
Overall, the two calculi appear to be almost equal in expressiveness3, since their translations involve linear complexity respectively (excluding the overhead for comparing names in the λ-calculus). Where differences are present (method lookup and variable substitution) they point out paradigm fundamentals.
Especially, the fast translations — we might also say native translations, since idiomatic constructs were used — demonstrated a very direct way of translating calculi constructs: Functions in theλ-calculus could be captured by objects operat- ing as closures. Objects in OPULUS could be represented by λ-terms and selector functions, e.g., an object with the fieldsa,b, andc—
λs.s a b c
— receives a message (selector) “b” by being applied toλx y z.y, yielding fieldb. Finally, one may note that fast and standard emulation of theλ-calculus are very close to each other (see table 3.6 on the facing page). This may hint to the fact that objects are a good abstraction for simulation. The direct simulation of λ-calculus was almost as efficient as the version that exploited native features of OPULUS.
Also, we may recall that an object-oriented languages would not need linear complexity to look upλ-variables in the environment. In contrast, an implementa- tion of OPULUS in a functional programming language will still need linear com-
plexity to look up names, unless further efforts like introducing method lookup arrays or similar machinery is introduced.
Before we derive further conclusions from the translation complexities found here, a small investigation is in order. Apparently, translation complexities depend
on the translation strategies. What if a better scheme, say for translating OPU-
LUS into theλ-calculus, could be found? Is it not possible that one calculus is still superior but we have not found the right translation scheme yet? The fact that both translations exhibit mostly linear characteristics gives us confidence that we found optimal translations and future translation schemes will not affect the over- all result. But what about sub-linear translations? Are they possible? Consider, a translation from a RAM model with just numerical addition into one that features multiplication also. When addition loops are converted into multiplications this could result into a sub-linear translation. However, the reverse translation would obviously be super-linear. If one our translations could be optimized to a sub-linear translation, the other one could not possibly have linear characteristics right now. Proof by false assumption: Assume the forth translation to be sub-linear and the back translation to be linear. With repeated back and forth translations a program could be made infinitely faster. As the conclusion is obviously false, so must be the assumption [Thierbach97].
In summary, it is certainly not a reverse logic approach to aim at subsuming functions with objects. While the other way round is not impossible either it ap- pears less natural. Moreover, when leaving the level of calculi, it appears more rea- sonable to subsume reduction semantics with state than to “ruin” the basic assump- tion of referential transparency with the introduction of stateful objects. Note, how- ever, the striking correspondence of some well-known combinators with impera- tive statements: S≡“:=”,K≡“const”,I≡“goto”, andCB≡“;” [Hudak89], sug- gesting how to emulate imperative features in a declarative language. I will con- tinue the discussion about the conflict between state and referential transparency in section 4.1.1 on the next page and section 4.2.1 on page 57.
4
Conflict & Cohabitance
When two worlds collide the question is whether they cancel out or enlighten each other. – me
N
ow that we know the foundations of both functional and object-oriented programming and convinced ourselves that subsuming functional pro- gramming with an object-oriented language works fine at the calculus level it is time to refocus our overall goal. Part II starting at page 85 is going to present functional concepts made amenable to object-oriented design by captur- ing them in design patterns. But which functional concepts are appropriate? This chapter discusses functional and object-oriented concepts that seem to be at odds with each other (section 4.1) and shows a path of possible integration (section 4.2 on page 57) partly reconciling conflicts discovered in the following section.
4.1 Conflict
Although, the preceding chapter gave us confidence that an integration of func- tional concepts into an object-oriented language is viable it might be the case that incommensurable properties prevent an integration at the programming language level. Indeed, the following sections reveal some immediate oppositions and re- dundancies.
4.1.1 Oppositions
This section discusses diametral oppositions of the functional and the object- oriented paradigm.
4.1.1.1 Semantic model
As elaborated in section 1.2.2 on page 11 functional programming is founded on reduction semantics, i.e., excludes the presence of side-effects. Object-orientation, however, relies on stateful objects (see section 2.2.6 on page 37). This is a serious conflict. Abandoning either reduction semantics or stateful objects seems to de- stroy one of the paradigm foundation piles respectively. Actually, the integration of functional and object-oriented features amounts to combining both declarative and algorithmic language paradigms (see figure 4.1 on the next page).
procedural object-oriented functional logic declarative algorithmic Programming Paradigms
Figure 4.1: Classification of programming paradigms
4.1.1.2 Decomposition
There is a fundamental dichotomy between the decomposition strategies in func- tional and object-oriented programming on two levels: On a large scale we must decide whether to decompose a software system into functions (section 1.2.1 on page 10) or into objects (section 2.2.1 on page 32) [Meyer88]. On a small scale we have to choose between data abstraction (section 1.2.1 on page 10) or procedural ab- straction (section 2.2.1 on page 32) [Cook90]. Either we package data constructors and use functions that dispatch on those or we package functions and distribute them to their respective constructors.
4.1.1.3 Evaluation
In section 1.2.2 on page 11 and section 1.2.4 on page 14 we have learned that normal- order reduction or lazy evaluation has desirable properties. This is the case as long as side-effects are not allowed. Side-effects do not fit with lazy evaluation with its unintuitive and data dependent evaluation order. For this reason, functional languages with side-effects (e.g., ML or UFO) use eager evaluation. Then, side-
effects of the function arguments will appear prior to that of the function itself. It has also been said that laziness can conflict with dynamic binding. In order to dynamically bind on an argument it needs to be evaluated anyway without any option of delay [Sargeant95].
4.1.1.4 Encapsulation
Data abstraction, i.e., exposing data constructors to functions of an abstract datatype works well with pattern matching (section 1.2.5 on page 16) but is at odds with encapsulation (section 2.2.2 on page 32). This is not an issue of object en- capsulation but has been recognized in the functional programming community as well [Wadler87]. A function that uses pattern matching for a datatype is exposed to its representation — as it accesses its constructors — and is subject to change whenever the datatype has to be changed. There has been a number of propos- als how to avoid this drawback, each trying to improve on the weaknesses of the former [Wadler87, Thompson89, Burton & Cameron94, Gostanza et al.96].
4.1.2 Redundancy
It is obvious that opposing concepts exclude their integration. All the same, it is of no use to integrate concepts that are redundant to each other. Concepts must complement each other otherwise a software solution will become an incoherent mix of styles which is hard to understand but does not justify its diversity with corresponding properties.
4.1.2.1 Parameterization
Higher-order functions (see section 1.2.3 on page 12) and inheritance (see sec- tion 2.2.3 on page 33) can both be used for behavior parameterization [K ¨uhne95b]. The Template Method pattern uses subclassing to achieve behavior parameteriza- tion akin to higher-order functions. Unless the two mechanism do not yield soft- ware solutions with different properties and no further complement of one to the other can be found, one should be dismissed.
4.1.2.2 Dispatch
Pattern matching (section 1.2.5 on page 16) and dynamic binding (section 2.2.5 on page 35) can both be used to decompose a function into partial definitions and to select the appropriate portion when executing the function. Both mechanisms di- vide a function according to the constructors it operates on. While pattern matching keeps the patterns at one place, dynamic binding distributes it to the constructors. It should be clear whether two code selection mechanisms are needed.
4.2 Cohabitance
If we literally agreed to all the arguments made in the previous sections it would be time to abandon any paradigm integration at all. Luckily, many points made above that at first appear excluding are in fact complementing. In the following, I propose how to subsume and integrate functional concepts into those of the object-oriented paradigm. The last section goes beyond feasibility and presents synergistic effects between the two paradigms.
4.2.1 Subsumption
Subsuming functional concepts into object-oriented ones means to find ways to express them without changing an object-oriented language.
4.2.1.1 Pure functions
Even without motivation from functional languages it has been suggested to use side-effect free functions only [Meyer88]. The so-called command-query separation principle prescribes to use side-effects for commands only. For instance the code,
a:=io.nextChar; b:=io.nextChar;
should be written as a:=io.lastChar; io.nextChar; b:=io.lastChar; io.nextChar;
because the latter version clearly differentiates between reading input (query) and advancing the input stream (command). Thus, it is possible to assign a charac- ter from the input stream to two variables without accidently advancing the input stream in between.
Concerning the side-effects of functions one might make a difference between abstract state and concrete state of objects. The latter may change without affecting the former. For instance, a complex number may have two states of representation: A Polar coordinates state for fast multiplication and a Cartesian coordinates state for fast addition. A complex number object may autonomously switch between these concrete states without affecting the abstract state, i.e., its complex number value. Also, some side-effects that do not affect the behavior of a system can be allowed too. The protocoling of executed commands [Dosch95] is a typical example of harmless side-effects.
As a result, the command-query separation principle reconciles state with re- duction semantics and also state with lazy evaluation. The reduction semantics of functional languages is embedded into an imperative environment by restrict- ing functions to query functions only. Especially, with the interpretation of objects being sub-states of a system [K ¨uhne96a] the configuration of side-effect free func- tions operating on system states is close to the design of John Backus’ AST1 sys- tem [Backus78] with its main-computations between system states.
Furthermore, when functions do not cause state changes and also are not influ- enced by state changes (see section 4.2.2.2 on the facing page) they may be executed in any order. This opens up the possibility of lazy evaluation again.
4.2.1.2 Dispatch
We have seen that pattern matching is responsible for many subtleties (section 1.3.2 on page 21) and problems (section 4.1.2.2 on the preceding page) without a real im- pact on software engineering properties (section 1.2.5 on page 16). We therefore de- cide to abandon pattern matching in favor of dynamic binding which is capable of achieving the same but in addition provides further opportunities (see section 2.2.5 on page 35).
I admit it is unfortunate to loose the nice syntax of pattern matching and to be forced to work with several classes (representing constructors) in order to deal with one function. Notwithstanding, the question of how to present and manipulate a function seems to be more a matter of tools rather than language [K ¨uhne96b]. Al- though functions are distributed over objects they can be presented to the program- mer as a single definition with an appropriate tool (see the epilogue on page 261 for a further discussion).
4.2.2 Integration
In contrast to subsumption, integration demands for adaptions of the host language in order to encompass functional concepts.
4.2.2.1 Evaluation
We already regained functions for lazy evaluation (section 4.2.1.1 on page 57) but may also want to have lazy object state semantics too. Otherwise, internal object state might be computed without being requested at all. One approach to achieve lazy effects relies on a refined state monad concept [Launchbury93, Launchbury & Jones94]. Another opportunity is to use models from concurrent object-oriented languages and to propose lazy message mailboxes for objects. In the context of this dissertation we will be satisfied with lazy functions only, though. A short note is in order regarding the remark that dynamic binding does not co- habit with lazy evaluation (see section 4.1.1.3 on page 56). First, dynamic binding only requires to know the type the receiver which amounts to a partial evaluation only. Second, function application is strict in its first argument anyway. One might evaluate arguments first but eventually the function to be applied must be eval- uated to a function abstraction. Therefore, dynamic binding poses no additional requirements that would render lazy evaluation useless.
4.2.2.2 Closures
Section 3.4 on page 49 demonstrated how to use objects as closures in order to im- plement functions with lexical scoping. Closures capture the values of variables at their declaration and/or application environment as opposed to the point of exe- cution. That is why, one can safely use closures in conjunction with lazy evaluation (see section 4.2.1.1 on page 57). Once applied to values closures do not depend on state changes anymore.
Why do closures need to be integrated rather than subsumed? Most object- oriented languages do not allow free functions2 and require class definition over- head for closures which especially gets worse when currying should be supported. Moreover, few object-oriented languages (e.g., SMALLTALK and JAVA) provide
mechanisms for anonymous closures (e.g., blocks [Goldberg & Robson83] and in- ner classes [Sun97] respectively).
4.2.3 Synergy
Synergy occurs when two or more concepts complement each other in a way that results in properties that cannot be described by the sum of the separated concepts.
4.2.3.1 Parameterization
Section 4.1.2 on page 57 postulated to either surrender higher-order functions or inheritance unless a good reason not to do so can be found. This section argues that both concepts are necessary and even amplify each other. We will proceed