PLE lecture notes -- Life

Life stands for Logic, Inheritance, Functions, and Equations. It integrates the defining characteristics of the three styles we have seen: Thus Life provides a constructive proof that these ideas are orthogonal.

We will present this language as an extension to Prolog which closes the language circle back to Haskell and Self. We will also avoid the usual term-itis, by using the more familiar "object" instead of "psi-term" and "class" instead of "sort".

Focal points

Values

As in Self and Sather, every value has a class. Standard classes:
bool
true or false

numbers: int and real
e.g. 42 and -5.66

string
Strings are a built-in class; they are not lists. There is no class for characters.
"hello there"

list
[x|y] is sugar for cons(x,y) and [] is sugar for nil. Both nil and cons are subclasses of list.
cons(1, cons(2, cons(3, nil)))  % the sequence 1,2,3
[1|[2|[3|[]]]]                  % sugar for above
[1,2,3]                         % more sugar

There are no anonymous functions in Life, and any object can be used as a tuple. Typing is dynamic.

Object-orientation

Objects

An object is a collection of named slots. Slots are accessed via dot notation. All objects have a special "root" slot which holds their class, however, this slot cannot be read or written after creation. To create an instance of a class, just call the class as a function, i.e. the class acts like a constructor (cf Python). Unlike Python, Life does not treat classes as values. Life shuns declarations: reading or writing a nonexistent slot implicitly creates that slot; using an unknown class name implicitly creates that class. Examples:
F <- foo      % class foo is implicitly created; F is an instance
F.x <- 3 
F.y <- "hi"   % undeclared slots are untyped
root_sort(F)     % returns 'foo'

N <- 3        % '3' is a class whose instances are the number 3
N.next <- 4   % N is an object, so we can add slots to it
F.x =:= N        % true; arithmetic comparison succeeds
F.x === N        % false; object comparison fails
F.x === 3        % false; object comparison still fails
In the second example, N is an object whose root slot is '3'. Conceptually, there is a class for every boolean, number, and string. Using one of these classes in an expression makes a new instance, which can be treated like any object. Consequently, separate uses of '3' in the program are separate instances of the class '3' (a subclass of int), whose slots can be modified independently. Arithmetic comparison only checks root slots.

To fill in slot values at instantiation-time, supply arguments when calling the class. Life allows keyword arguments in function calls; here the keywords will be used as slot names. Otherwise the slot names will be numeric. Examples:

X <- date("friday", 13)              % implicit numeric labels
X <- date(1=>"friday", 2=>13)        % same as above
X <- date(day=>"friday", number=>13) % alphabetic labels
X.day                                % "friday"

Classes

To declare properties of a class, use "::". Properties include required slots and constraints between slot values. Undeclared classes like date above have no required slots or constraints. Examples:
:: rectangle(length => real, width => real).    % required slots
:: X:rectangle | X.area = X.length * X.width.   % constraint
X <- rectangle(length => 5, area => 20)         % create a new rectangle
X.width                                         % 4
This example also shows that arithmetic operations are reversible. If we don't specify all of the required slots, they are filled with empty instances of type real. These slots can only be replaced by subclasses of real, to be explained below.

Classes can inherit properties from each other using "<|". For example, here are some built-in relationships:

1 <| int.
-1 <| int.
5.6 <| real.
"hello" <| string.
true <| bool.

int <| real.
foo(x=>5, y=>6) <| foo(x=>5).
Inheritance also establishes type-conformance: the last line says that extra slots do not change an object's type.

For example, we can define the class of prime numbers:

prime <| int.
:: I:prime | is_prime(I)
Where is_prime is a primality-testing predicate. A function which takes a prime argument will check this condition at runtime.

Unification

Classes are used as types during object unification. To unify two objects, unify all slots in common and extend each object with the slots of the other. This includes the root slot, so we also need a rule to unify classes. The rule chosen by Life is to pick the largest common subclass. (This has unusual ramifications on multiple inheritance, which we will explore in the treasure hunt.) For example, given these declarations:
13 <| int.
car <| vehicle.
car <| fast.
Expression (U) Unified with (V) Produces binding
100 int root(V) : 100
5.6 int fail
vehicle(wheels=>4) fast root(U) : car, root(V) : car, V.wheels : 4
int(luck=>"bad") 13(roman=>"XIII") root(U) : 13, U.roman : "XIII", V.luck : "bad"
Thus if we bind a variable to int, it can only be later unified with integers. This is how names are typed in Life.

Functions

In Prolog, functions were replaced by predicates, which use unification to bind output arguments. Life retains Prolog-style predicates but adds Haskell-style functions, which can return a non-boolean value. For example:
fact(0) -> 1.
fact(N:int) -> N * fact(N-1).
Functions are first-class values, though they must be named:
map(F,[]) -> [].
map(F,[H|T]) -> [ F(H) | map(F,T) ].

map(fact, [1,2,3])        % returns [1,2,6]

Matching vs. unification

If two-way unification were used for function calls, fact(X) would bind X to 0 and return 1. The problem here is that Life doesn't know in general when a function has "succeeded". Therefore functions are called using one-way matching, versus two-way, unification: unbound arguments cannot be bound by a function signature.

When a function "needs" an argument value, but that argument is unbound, the function will suspend until the argument value is known. Thus an unbound argument corresponds to a "promise". The caller will continue to execute, but if it "needs" the result of the function, it too will suspend. A function "needs" an argument if:

Predicates, by contrast, never need their arguments. Thus we have a syntactic and semantic rift between functions and predicates.

Examples:

const1(X) -> 1.    % always returns 1, doesn't need X
const1(Y)          % returns 1, without binding Y
const1(1/0)        % division by zero error; function is still strict!

X=5, Y=fact(X).    % Y gets 120
Y=fact(X), X=5.    % Y also gets 120; suspension makes order irrelevant

Life supports nonlinear patterns: a tag may occur twice, to force equality between arguments. However, it does not support general-purpose constraits the way predicates and Haskell's guards do.

Keyword currying

Functions in Life are curried: if called with a subset of their required arguments, they return a function for the remaining arguments. However, as mentioned earlier, functions take keyword, rather than positional arguments. The positional syntax foo("hello", 5.6) is really just syntactic sugar for foo(1=>"hello", 2=>5.6). Thus Life uses keyword, rather than positional, currying: if called with a subset of the required keywords, a function returns another function for the remaining keywords. For example, foo("hello") returns a function for the keyword '2':
foo("hello")(5.6)           % error
foo("hello")(2=>5.6)        % correct
foo(2=>5.6)("hello")        % also correct
area(height=>4)(length=>5)  % returns 20
Like positional currying, keyword currying does not support a variable number of arguments or optional arguments.

Non-generality

Function signatures look very much like objects. For example, this function computes the area of a rectangle:
area(length => L:real, height => H:real) -> L*H.
area(length => 4, height => 5)     % returns 20
However, in the body of area, the labels length and height cannot be used to denote their respective arguments; instead we must use the tags L and H. With an object definition, e.g. foo(length => 4, height => 5), the components can be referred to as foo.length and foo.height. However, area.length and area.height don't parse in Life. But if we curry area, e.g. F <- area(height => 5), then F.height is 5. If function signatures really were objects, then general purpose guards might be provided by class constraints.

Unlike the other non-logical languages we have seen, Life has no block structure to speak of. There is a special facility for modules, but these cannot be nested. Furthermore, no syntactic or semantic connection is made between modules and objects, even though they are both a kind of naming environment. The hidden root slot is also reminiscent of Python's approach to object-orientation, which Self simplified by exposure of the slot.

Three kinds of names

Life uses all three kinds of naming discussed in the linguistics lecture:

Reflection

Life provides reflection capabilities similar to Self, with the ability to reify objects as lists. However, the root slot must still be treated specially. There are also the functions parents and children to query the type graph. There is no built-in mechanism to reify the type graph, in such a way that modifying the reified data structure modifies the type graph. Nevertheless, Life's dynamic approach to typing is more flexible than in other object-oriented languages, e.g. Sather.


PLE Home page
Last modified: Tue Oct 12 19:12:32 EDT 1999