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".
[x|y]is sugar for
is sugar for
consare subclasses of
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.
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 failsIn the second example,
Nis 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"
::". Properties include required slots and constraints between slot values. Undeclared classes like
dateabove 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 % 4This 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_primeis a primality-testing predicate. A function which takes a
primeargument will check this condition at runtime.
13 <| int. car <| vehicle. car <| fast.
|Expression (U)||Unified with (V)||Produces binding|
|100||int||root(V) : 100|
|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"|
int, it can only be later unified with integers. This is how names are typed in Life.
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]
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:
factneeds its argument for this reason.
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.
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 20Like positional currying, keyword currying does not support a variable number of arguments or optional arguments.
area(length => L:real, height => H:real) -> L*H. area(length => 4, height => 5) % returns 20However, in the body of
area, the labels
heightcannot 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
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.
<<-. These assignments are like side-effects; they will not be undone by backtracking and they can be repeated to the same variable.
childrento 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.