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".
true
or false
42
and -5.66
"hello there"
[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.
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,
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"
::
".
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 % 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_prime
is a primality-testing predicate.
A function which takes a prime
argument will check this
condition at runtime.
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" |
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:
fact
needs its argument for this reason. fact(X)
will suspend.
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.
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
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.
<<-
. These assignments are like side-effects;
they will not be undone by backtracking and they can be repeated to the same
variable.
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.