- Modern Programming: Object Oriented Programming and Best Practices
- Graham Lee
- 1437字
- 2021-05-21 22:05:58
Opposing Functional Programming
[An] important aspect of functional programming is that functions do not change the data with which they work [...] Object-oriented imperative languages such as C, Java, or Python change their state as they run.
Neil Savage, (Using Functions for Easier Programming — https://dl.acm.org/citation.cfm?id=3193776)
Many programmers define themselves through their tools, and therefore define themselves as against certain other tools. If you are a .NET programmer, then you do not use Java. If you are a native mobile programmer, then you do not use JavaScript. If you are a React programmer, then you do not use Angular. An affiliation with one tool automatically means a disaffiliation with others.
Such partisanship is a confirming example of Sayre's law: the arguments are so fierce because the stakes are so low. For people who supposedly work in a field of rationality and science, we're really good at getting emotionally brittle when somebody wants to use a different library, language, text editor, or whitespace symbol than the one we have chosen.
This fierce disagreement over strongly defended similarities extends to the programming paradigm, too. If you are an object-oriented programmer, then your mortal enemy is the functional programmer—http://www.sicpers.info/2015/03/inspired-by-swift/, and vice versa.
Messages Are Just Requests
Not so fast! Recall the working definition of objects I have used throughout the antithesis: an object is an isolated, independent computer program that communicates with other programs by passing messages. This tells us nothing about how to build those isolated, independent computer programs. Particularly, there is no mandate to have mutable state anywhere. The following interface works as a messaging interface for a time-varying list:
public interface MutableList<T> {
void setElements(T[] elements);
void appendObject(T element);
void removeObject(int index) throws OutOfBoundsException;
void replaceObject(int index, T element) throws OutOfBoundsException;
int count();
T at(int index);
};
And so, does this one:
public interface TemporalList<T> {
void setInitialState(T[] elements);
void appendObject(T element, Time when);
void removeObject(int index, Time when) throws InconsistentHistoryException;
void replaceObject(int index, T element, Time when) throws InconsistentHistoryException;
void revertMostRecentChange(Time beforeNow);
int count(Time when);
T at(int index, Time when);
};
In the first, time in the list's lifespan is modeled using successive states of the computer memory. In the second, time in the list's lifespan is modeled explicitly, and the history of the list is preserved. Another option is to model evolution using different objects, turning time into space:
public interface ImmutableList<T> {
ImmutableList<T> addObject(T element);
ImmutableList<T> removeObject(int index) throws OutOfBoundsException;
ImmutableList<T> replaceObject(int index, T element) throws OutOfBoundsException;
int count();
T at(int index);
}
Now the list looks a lot like a sort of a functional programming list. But it's still an object. In each case, we have defined what messages the object responds to but, remembering the section on Telling an Object What to Do, we have not said anything about what methods exist on that object, and certainly not how they are implemented. The MutableList and TemporalList interfaces use Bertrand Meyer's principle of Command-Query Separation, in which a message either instructs an object to do something (like add an element to a list) or asks the object for information (like the number of elements in a list), but never does both. This does not automatically imply that the commands act on local mutable state though. They could execute Datalog programs, or SQL programs, or be stored as a chain of events that is replayed when a query message is received.
In the ImmutableList interface, commands are replaced by transforms, which ask for a new list that reflects the result of applying a change to the existing list. Again, no restriction on how you implement those transforms is stated (I could imagine building addObject() by having a new list that delegates every call to the original list, adding 1 to the result of count() and supplying its own value for at(originalCount); or I could just build a new list with all of the existing elements and the new element), but in this case, it's clear to see that every method can be a pure function based on the content of the object and the message parameters.
We can see that "pure function based on the content of the object and the message parameters" is the same as "pure function" more clearly by rewriting the interface in Python syntax (skipping the implementations):
class ImmutableList:
def addObject(this, element):
pass
def removeObject(this, index):
pass
def replaceObject(this, index, element):
pass
def count(this):
pass
def at(this, index):
pass
It's now easier to see that each of these methods is a pure function in its parameters, where this/self is a parameter that's automatically prepared in other languages (or a part of the method's environment that's automatically closed over in others).
Nothing about message-passing says, "please do not use functional programming techniques."
An Object's Boundary is Just a Function
The following subsections were deeply informed by the article Objects as Closures: abstract semantics of object-oriented languages — https://dl.acm.org/citation.cfm?id=62721, which builds this view of objects much more rigorously.
The interface to an object is the collection of messages it responds to. In many cases, this is backed by a collection of methods, each with the same name as the message selector that will invoke it. Not only is this the easiest thing to do, it's also an implementation constraint in many programming languages. The preceding Python implementation of ImmutableList can be visualized in this table:
Figure 3.1: Visualization of ImmutableList after implementation
This table can equivalently be replaced by a pure function of type Message Selector->Method to Invoke. A trivial implementation of the function would look up its input in the left-hand column of the table and return the value it finds in the same row in the right-hand column. An implementation of ImmutableList doesn't need to have any methods at all, choosing functions based on the message selector:
class ImmutableList:
def __init__(this, elements):
this.elements = elements
def __getattr__(this, name):
if name == "count":
return lambda: len(this.elements)
elif name == "at":
return lambda index: this.elements[index]
# ...
Using this object works the same way as using an object where the methods were defined in the usual way:
>>> il = ImmutableList([1,2,3])
>>> il.count()
3
>>> il.at(0)
1
>>>
So, whichever way you write out an object, its methods are functions that have access to (close over) the object's internals, and its message interface is one such function that uses the message selector to choose which method to invoke.
Freed from the fetters of the language's idea of where methods live, we see that the function to look up implementations from selectors can use any information available to it. If the object knows about another object, it can send the message on to the other object, send a different method in its place, or it could compile a new function and use that. The important idea is that an object is a function for finding other functions.
That Function-Like Boundary? Actually, a Closure Over the Constructor Arguments
Our ImmutableList has a constructor method called __init__, which sets up the initial state of the object using its arguments, and then the message-finding __getattr__ function, which chooses functions to respond to the messages that are sent to the object.
An equivalent way to arrange this is to have the constructor function return the message-finding function as a closure over the constructor's arguments (and any transformation implied in "setting up the initial state of the object" can be arranged using local variables that are captured in the closure, too). So, all in all, an object is a single higher-order function: a function that captures its arguments and returns a closure over those arguments that accept messages and then chooses a method to execute the code:
(constructor arguments) -> message -> (method arguments) -> method return type
Sticking with Python, and using this insight, ImmutableList is reduced to a single expression:
def ImmutableList(elements):
return type('ImmutableList',
(object,),
{'__getattr__':
(lambda this, name:
(lambda: len(elements)) if name=="count"
else (lambda index: elements[index]) if name=="at"
else False)
})()
By the way, this demonstrates why so many object-oriented languages don't seem to have a type system. If "everything is an object," then even in the most stringent of type systems, everything is a message->method function, so everything has the same type, and everything type checks.
The preceding definition of ImmutableList does escape the "everything is an object" type scheme by ending with the phrase else False, meaning "if I didn't find a method, return something that isn't callable, so the user gets a TypeError." A more complete object system would have the object send itself a doesNotRespond message here, and no breaking out into Python's usual world of computation would occur.