This post is the first in three that will discuss how AI programming can be integrated into software applications. The second will be on Model-View-Controller, and the third on roles that AI software can play in conventional applications.
Object-oriented design, I think, really boils down to the application of two ideas in organizing programs. The concept of an Abstract Data Type (ADT) and polymorphism. The ADT idea is by far the most important of the two.
Abstract Data Types (ADTs)
The ADT approach is to specify the behavior of a program in terms of one of more data types and logical assertions (or contracts) about operations on objects of those types. The ADT specification is then implemented as actual statements in a programming language.
Software engineering is mainly the task of allowing humans to create enormously complex artifacts without being overwhelmed by complexity. The key thing the ADT idea gets us is the ability to think about our programs at higher levels of abstraction. We can think about the ‘the network interface’ rather than the huge pile of procedures, if, while, and other statements that make up the network interface.
Because ADTs are described in terms of logical specifications we can reason and construct arguments about them without looking at program code. It is really common to define one ADT in terms of other, lower level ADTs. We can do this only because we can construct arguments about the behavior of the lower level types.
When the specification is separate from the implementation we can test a particular implementation to make sure conforms to the ADT contract. It turns out to be difficult to test an implementation for complete conformance, but most of the time we can get by with unit testing: picking a good set test cases and making sure the implementation does the expected thing for those.
The other advantage is ADTs is that we can have more than one implementation of a given specification. If we need to change something without altering conformance with the specification we can do so without unintentionally changing the behavior of other parts of the program.
We may design more than one ADT to implement a common set of operations. This is done in several ways in object oriented programs: inheritance allows us to group a number of types into a hierarchy that share a set of common operations. Traits or interfaces allow use to specify that some set of (possibly) unrelated types all have the same set of operations.
The key idea is that if we can have several ADTs that, in addition to their own specific operations, implement a shared specification. The ADTs A and B might each imply the specifications for a third ADT C. When we get this situation we can reason about and write code about type C objects without knowing if the object is “really” an A or B instance.
For example, say the COLLECTION type represents a collection of numbers. type SET a set of numbers (any one number can only be included once), and type LIST a list of numbers (ordered, and a single number can appear more than once). Both SET and LIST are collections, so both types imply the behavior specified as type COLLECTION.
Say COLLECTION specifies an operation ‘count’ yielding a count of elements. SET and LIST are each likely define the ‘count’ operation differently, but by having the shared specification for ‘count’ we can write programs that use the ‘count’ operation on any object that implements COLLECTION.
Contracts vs Research Goals
Object-oriented design provides tools for humans to think about and specify the behavior of programs in a clear, unambiguous way. The key tool is to provide a way for us to substitute logical abstractions (contracts) for knowledge of the detailed behavior of actual program code. But what happens when our program may or may not be able to realize a specification? There are three cases that are interesting.
- When our programs may or might not succeed in meeting a specification. This is typical of many programs that use search to find a solution. Operations are applied in hope that they yield a state that is closer to a goal, but no guarantees!
- When our programs are designed to learn from data or interaction. The behavior of the program in relation to our goals is greatly dependent on the training data or interaction history.
- When our goals are not well defined. It sounds funny that we would go there, but there are many legitimate uses for programs that just try out an idea.
For the non-AI software engineer, the goals of the programer are often exactly the conditions that are placed in the specification. This is possible just because a non-AI programmer expects to have methods that will realize her or his goals, and will often shy away from goals that are not as well defined or realizable in software.
An AI programmer must deal with the gap between research goals (for example I want my software to produce interesting, original ideas in math) and the program specification. In fact this difference is precisely the point of AI. The AI programmer makes a claim about a program and its specification being better or worse are implementing a specific research goal. This is an empirical claim that can only tested by comparing the program with other alternatives. it is easier to make the case if both the program specification and the research goals are well defined.
In all three cases, object oriented design and contracts are still useful for reasoning complex software. We can do this by developing bracketed specifications around realizable expectations for the behavior of a specific program. Sometimes we will have a software engineering hat on and look for specific tests that a program will pass to be correct, and in other cases put our research hat on and make claims about the relationship between a program’s behavior and larger research goals.