The aim of this project is to implement an extension of the Opal compiler, that allows the use of reflections. Reflections have surfaced recently in various imperative and object orientated programming languages. For a functional programming language like Opal, the implementation of a reflection facility is both challenging and educative: In a strictly typed functional programming language the management of runtime type information seems to be an alien objective -- however it turns out that such information can be incorporated in a very elegant fashion into the strict type system.
Informally, a reflection is an object which allows you to enquire information about a value at runtime. The information that can be enquired encompasses
We will now have a look at the way types are expressed in functional
programming languages and we will see how reflections fit into this
framework formally. Consider a normal object like the value 5
of type
nat
. This value has one specific atomic type, namely nat
. Now,
consider the function succ
which has type nat -> nat
. Still,
succ
can be thought of as a value which has one specific type, namely
nat -> nat
.
Contrast this with the identity function id[alpha]
which has type
alpha -> alpha
regardless of the actual value of
alpha
. In this case, one says that id[alpha]
is
polymorphic and we can no longer assign a basic type to the
expression. However, the identity function obviously has a well defined
type, namely alpha -> alpha
for any specific alpha
. This
is expressed formally, by saying that the identity function has type
forall alpha. alpha -> alpha
.
So what about reflections? A reflection of a nat
value does not have
type nat
itself. However, there is a nat
stored somewhere in
the reflection. This is a little bit like the identity function: By itself,
it does not have type nat -> nat
-- but if you provide the type
nat
, the compiler can turn the general forall alpha. alpha ->
alpha
into the specific nat -> nat
. Likewise, a reflection of a
nat
can be turned into a nat
if you let the compiler apply the
same unpacking operation as for the identity function. However, this
works only for one type, namely nat
. Formally, a reflection is an
existentially bound type: a reflection has type exists
alpha. alpha
. A reflection is simply the assertion that there exists at
least one type (in our case nat
) which we can instantiate the
reflection with, yielding a value of that type. For all wrong types, this
instantiation will not be possible.
In our implementation, we did not change the existing Opal system any more than necessary. Specifically, we did not make the compiler aware of existentially bound types. Instead, reflections are build on top of the existing system.
A reflection has type reflection
(this is just a normal Opal type and
its our way of expressing that something actually has type exists
alpha. alpha
). If you reflect
a value like 5
you get such a
reflection
of the value 5
. Thus the expression
reflect(5)
has type reflection
.
A reflection is a normal Opal value which you can pass around as any other
value. Applying the function type
to a reflection yields the
reflected value's type. A simple usage of reflections is given in the
following code:
FUN tellType : value -> denotation -- This functions returns a verbal description of the type of -- some given value, using the reflection mechanism DEF tellType (refl) == IF type (refl) = type (reflect (0)) THEN "natural number" IF type (refl) = type (reflect ("")) THEN "denotation" -- ... FI DEF main == writeLine (stdOut, "5 has type " ++ tellType (reflect (5))); writeLine (stdOut, "'Hallo' has type " ++ tellType (reflect ("Hallo")))
Essentially, the function tellType
checks whether the type of the
value reflected by the variable refl
is the same type of the value 0
(which we know to have type nat
). Note, that you could not have
written something like IF type(relf) = nat THEN ...
. The reason is,
that reflections are implemented and used like any other Opal value. So, the
function type
returns a value of type typeReflection
(our
abstraction of the Opal type system), whereas nat
is an actual Opal
type and no Opal value of type typeReflection
.
The most important operation you will be interested in, is instantiating a
given reflection with the type hidden by the existential quantor and getting
the original value. This operation is done by asking a reflection, if it
reflects?
a given type. If so, the original value is
returned (and otherwise the operation fails gracefully). Testing numerous
different types, allows you to construct very general function which work
for many different types.
We can now give a nice example of the usage of reflections:
FUN print : value -> com[void] DEF print (refl) == IF i avail? THEN writeLine (stdOut, `(cont(i))) IF c avail? THEN writeLine (stdOut, `(cont(c))) IF str avail? THEN writeLine (stdOut, cont(str)) ELSE writeLine (stdOut, "unknown type") FI WHERE i == refl reflects? [int] c == refl reflects? [char] str == refl reflects? [string] DEF PrettyPrint == writeLine (stdOut, "Writing an integer: ") & print (reflect (5)) & writeLine (stdOut, "Writing a string: ") & print (reflect (!("Yes!")))
As can be seen, we can essentially write a single function print
which will print objects of just about any type. This allows you to write
functions that will operate differently for different input types in a
statically typed language like Opal.
The curious usage of square brackets for the function reflects?
is
explained later.
Until now, we have had to call the function reflect
every time we
wanted to use a reflection. This may become bothersome, especially with
functions like print
which we intend to use often.
We might come up with the following idea: Let print
do the reflection
itself! Thus, we would like to have print
have type forall
alpha. alpha -> com[void]
. Then, print
could reflect its parameter
and then use the reflection mechanism to find out what alpha
actually
is. The nice thing is, this actually works, provided you add a the special
pragma DYNAMIC
to the signature of Print
. This pragma is only
needed if you intend to reflect values of parameter types (like alpha
in our case).
IMPLEMENTATION Print [alpha] /$ DYNAMIC [alpha] $/ FUN print : alpha -> com[void] DEF print (a) == IF d avail? THEN write (stdOut, cont(d)) IF n avail? THEN write (stdOut, `(cont(n))) -- ... ELSE write (stdOut, "unknown type") FI WHERE refl == reflect[alpha] (a) d == refl reflects? [denotation] n == refl reflects? [nat] -- ... IMPLEMETATION Main IMPORT Print COMPLETELY DEF main == print ("Hello World"); print (" 3 + 3 = "); print (3+3); print ("\n")
If you take this to its extreme, you wind up with interfaces in the Service subproject.
"Reflections in Opal" was a graduate students' project in the winter semester 1998/99 under supervision of Wolfgang Grieskamp. In our opinion, the final result is worth including it in the official OPAL release.
next node: User Subsystem : Reflections,
prev node: Function Index,
up to node: Table of Contents