next node: User Subsystem : Reflections,
prev node: Function Index,
up to node: Table of Contents


Survey of the project

Introduction

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

Reflections and formal types

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.

Usage of reflections

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.

Reflections of objects with polymorphic type

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.

History

"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