Chapter 6. Data Types

6.1. General [1]

A MDL object consists of two parts: its TYPE and its "data part" (appendix 1). The interpretation of the "data part" of an object depends of course on its TYPE. The structural organization of an object, that is, the way it is organized in storage, is referred to as its "primitive type". While there are many different TYPEs of objects in MDL, there are fewer primitive types.

All structured objects in MDL are ordered sequences of elements. As such, there are SUBRs which operate on all of them uniformly, as ordered sequences. On the other hand, the reason for having different primitive types of structured objects is that there are useful qualities of structured objects which are mutually incompatible. There are, therefore, SUBRs which do not work on all structured objects: these SUBRs exist to take full advantage of those mutually incompatible qualities. The most-commonly-used primitive types of structured objects are discussed in chapter 7, along with those special SUBRs operating on them.

It is very easy to make a new MDL object that differs from an old one only in TYPE, as long as the primitive type is unchanged. It is relatively difficult to make a new structured object that differs from an old one in primitive type, even if it has the same elements.

Before talking any more about structured objects, some information needs to be given about TYPEs in general.

6.2. Printed Representation [1]

There are many TYPEs for which MDL has no specific representation. There aren't enough different kinds of brackets. The representation used for TYPEs without any special representation is

#type representation-as-if-it-were-its-primitive-type

READ will understand that format for any TYPE, and PRINT will use it by default. This representational format will be referred to below as "# notation". It was used above to represent FUNCTIONs.

6.3.1. TYPE [1]

<TYPE any>

returns an ATOM whose PNAME corresponds to the TYPE of any. There is no TYPE "TYPE". To type a TYPE (aren't homonyms wonderful?), just type the appropriate ATOM, like FIX or FLOAT or ATOM etc. However, in this document we will use the convention that a metasyntactic variable can have type for a "data type": for example, foo:type means that the TYPE of foo is ATOM, but the ATOM must be something that the SUBR TYPE can return.


<TYPE 1>$
<TYPE 1.0>$
<TYPE +>$
<TYPE ,+>$

6.3.2. PRIMTYPE [1]


evaluates to the primitive type of any. The PRIMTYPE of any is an ATOM which also represents a TYPE. The way an object can be manipulated depends solely upon its PRIMTYPE; the way it is evaluated depends upon its TYPE.



6.3.3. TYPEPRIM [1]


returns the PRIMTYPE of an object whose TYPE is type. type is, as usual, an ATOM used to designate a TYPE.



6.3.4. CHTYPE [1]

<CHTYPE any type>

("change type") returns a new object that has TYPE type and the same "data part" as any (appendix 1).

<CHTYPE (+ 2 2) FORM>$
<+ 2 2>

An error is generated if the PRIMTYPE of any is not the same as the TYPEPRIM of type. An error will also be generated if the attempted CHTYPE is dangerous and/or senseless, for example, CHTYPEing a FIX to a SUBR. Unfortunately, there are few useful examples we can do at this point.

[CHTYPEing a FIX to a FLOAT or vice versa produces, in general, nonsense, since the bit formats for FIXes and FLOATs are different. The SUBRs FIX and FLOAT convert between those formats. Useful obscurity: because of their internal representations on the PDP-10, <CHTYPE <MAX> FIX> gives the least possible FIX, and analogously for MIN.]

Passing note: "# notation" is just an instruction to READ saying "READ the representation of the PRIMTYPE normally and (literally) CHTYPE it to the specified TYPE". [Or, if the PRIMTYPE is TEMPLATE, "apply the GVAL of the TYPE name (which should be a TEMPLATE constructor) to the given elements of the PRIMTYPE TEMPLATE as arguments."]



returns a VECTOR (chapter 7) containing just those ATOMs which can currently be returned by TYPE or PRIMTYPE. This is the very "TYPE vector" (section 22.1) that the interpreter uses: look, but don't touch. No examples: try it, or see appendix 3.

6.4.2. VALID-TYPE?

<VALID-TYPE? atom>

returns #FALSE () if atom is not the name of a TYPE, and the same object that <TYPE-C atom> (section 19.5) returns if it is.

6.4.3. NEWTYPE

MDL is a type-extensible language, in the sense that the programmer can invent new TYPEs and use them in every way that the predefined TYPEs can be used. A program-defined TYPE is called a NEWTYPE. New PRIMTYPEs cannot be invented except by changing the interpreter; thus the TYPEPRIM of a NEWTYPE must be chosen from those already available. But the name of a NEWTYPE (an ATOM of course) can be chosen freely -- so long as it does not conflict with an existing TYPE name. More importantly, the program that defines a NEWTYPE can be included in a set of programs for manipulating objects of the NEWTYPE in ways that are more meaningful than the predefined SUBRs of MDL.

Typically an object of a NEWTYPE is a structure that is a model of some entity in the real world -- or whatever world the program is concerned with -- and the elements of the structure are models of parts or aspects of the real-world entity. A NEWTYPE definition is a convenient way of formalizing this correspondence, of writing it down for all to see and use rather than keeping it in your head. If the defining set of programs provides functions for manipulating the NEWTYPE objects in all ways that are meaningful for the intended uses of the NEWTYPE, then any other program that wants to use the NEWTYPE can call the manipulation functions for all its needs, and it need never know or care about the internal details of the NEWTYPE objects. This technique is a standard way of providing modularity and abstraction.

For example, suppose you wanted to deal with airline schedules. If you were to construct a set of programs that define and manipulate a NEWTYPE called FLIGHT, then you could make that set into a standard package of programs and call on it to handle all information pertaining to scheduled airline flights. Since all FLIGHTs would have the same quantity of information (more or less) and you would want quick access to individual elements, you would not want the TYPEPRIM to be LIST. Since the elements would be of various TYPEs, you would not want the TYPEPRIM to be UVECTOR -- nor its variations STRING or BYTES. The natural choice would be a TYPEPRIM of VECTOR (although you could gain space and lose time with TEMPLATE instead).

Now, the individual elements of a FLIGHT would, no doubt, have TYPEs and meanings that don't change. The elements of a FLIGHT might be airline code, flight number, originating-airport code, list of intermediate stops, destination-airport code, type of aircraft, days of operation, etc. Each and every FLIGHT would have the airline code for its first element (say), the flight number for its second, and so on. It is natural to invent names (ATOMs) for these elements and always refer to the elements by name. For example, you could <SETG AIRLINE 1> or <SETG AIRLINE <OFFSET 1 FLIGHT>> -- and in either case <MANIFEST AIRLINE> so the compiler can generate more efficient code. Then, if the local value of F were a FLIGHT, <AIRLINE .F> would return the airline code, and <AIRLINE .F AA> would set the airline code to AA. Once that is done, you can forget about which element comes first: all you need to know are the names of the offsets.

The next step is to notice that, outside the package of FLIGHT functions, no one needs to know whether AIRLINE is just an offset or in fact a function of some kind. For example, the scheduled duration of a flight might not be explicitly stored in a FLIGHT, just the scheduled times of departure and arrival. But, if the package had the proper DURATION function for calculating the duration, then the call <DURATION .F> could return the duration, no matter how it is found. In this way the internal details of the package are conveniently hidden from view and abstracted away.

The form of NEWTYPE definition allows for the TYPEs of all components of a NEWTYPE to be declared (chapter 14), for use both by a programmer while debugging programs that use the NEWTYPE and by the compiler for generating faster code. It is very convenient to have the type declaration in the NEWTYPE definition itself, rather than replicating it everywhere the NEWTYPE is used. (If you think this declaration might be obtrusive while debugging the programs in the NEWTYPE package, when inconsistent improvements are being made to various programs, you can either dissociate any declaration from the NEWTYPE or turn off MDL type-checking completely. Actually this declaration is typically more useful to a programmer during development than it is to the compiler.)

<NEWTYPE atom type>

returns atom, after causing it to become the representation of a brand-new TYPE whose PRIMTYPE is <TYPEPRIM type>. What NEWTYPE actually does is make atom a legal argument to CHTYPE and TYPEPRIM. (Note that names of new TYPEs can be blocked lexically to prevent collision with other names, just like any other ATOMs -- chapter 15.) Objects of a NEWTYPE-created TYPE can be generated by creating an object of the appropriate PRIMTYPE and using CHTYPE. They will be PRINTed (initially), and can be directly typed in, by the use of "# notation" as described above. EVAL of any object whose TYPE was created by NEWTYPE is initially the object itself, and, initially, you cannot APPLY something of a generated TYPE to arguments. But see below.


#GARGLE *000000000001*
<SET B #GARGLE 100>$
#GARGLE *000000000144*
<TYPE .B>$


<PRINTTYPE type how>

<EVALTYPE type how>

<APPLYTYPE type how>

all return type, after specifying how MDL is to deal with it.

These three SUBRs can be used to make newly-generated TYPEs behave in arbitrary ways, or to change the characteristics of standard MDL TYPEs. PRINTTYPE tells MDL how to print type, EVALTYPE how to evaluate it, and APPLYTYPE how to apply it in a FORM.

how can be either a TYPE or something that can be applied to arguments.

If how is a TYPE, MDL will treat type just like the TYPE given as how. how must have the same TYPEPRIM as type.

If how is applicable, it will be used in the following way:

For PRINTTYPE, how should take one argument: the object being output. how should output something without formatting (PRIN1-style); its result is ignored. (Note: how cannot use an output SUBR on how's own type: endless recursion will result. OUTCHAN is bound during the application to the CHANNEL in use, or to a pseudo-internal channel for FLATSIZE -- chapter 11.) If how is the SUBR PRINT, type will receive no special treatment in printing, that is, it will be printed as it was in an initial MDL or immediately after its defining NEWTYPE.

For EVALTYPE, how should take one argument: the object being evaluated. The value returned by how will be used as EVAL of the object. If how is the SUBR EVAL, type will receive no special treatment in its evaluation.

For APPLYTYPE, how should take at least one argument. The first argument will be the object being applied: the rest will be the objects it was given as arguments. The result returned by how will be used as the result of the application. If how is the SUBR APPLY, type will receive no special treatment in application to arguments.

If any of these SUBRs is given only one argument, that is if how is omitted, it returns the currently active how (a TYPE or an applicable object), or else #FALSE () if type is receiving no special treatment in that operation.

Unfortunately, these examples are fully understandable only after you have read through chapter 11.

<COND (<OR <L=? .NUMB 0> <G? .NUMB 3999>>
       <RCPRINT </ .NUMB 1000> '![!\M]>
       <RCPRINT </ .NUMB  100> '![!\C !\D !\M]>
       <RCPRINT </ .NUMB   10> '![!\X !\L !\C]>
       <RCPRINT    .NUMB       '![!\I !\V !\X]>)>>$

<COND (<==? 0 .MODN>)
      (<==? 1 .MODN> <PRINC <1 .V>>)
      (<==? 2 .MODN> <PRINC <1 .V>> <PRINC <1 .V>>)
      (<==? 3 .MODN> <PRINC <1 .V>> <PRINC <1 .V>> <PRINC <1 .V>>)
      (<==? 4 .MODN> <PRINC <1 .V>> <PRINC <2 .V>>)
      (<==? 5 .MODN> <PRINC <2 .V>>)
      (<==? 6 .MODN> <PRINC <2 .V>> <PRINC <1 .V>>)
      (<==? 7 .MODN> <PRINC <2 .V>> <PRINC <1 .V>> <PRINC <1 .V>>)
      (<==? 8 .MODN>
       <PRINC <2 .V>>
       <PRINC <1 .V>>
       <PRINC <1 .V>>
       <PRINC <1 .V>>)
      (<==? 9 .MODN> <PRINC <1 .V>> <PRINC <3 .V>>)>>$

<PRINTTYPE TIME FIX> ;"fairly harmless but necessary here"$
<PRINTTYPE FIX ,ROMAN-PRINT>    ;"hee hee!"$
<+ 2 2>$

<EVALTYPE GRITCH LIST>  ;"evaluated like a LIST"$
#GRITCH (A <+ 1 2 3> !<SET A "ABC">)    ;"Type in one."$
#GRTICH (A 6 !\A !\B !\C)

    ;"When a HARRY is EVALed, return its first element."$
#HARRY [1 2 3 4]$

<NEWTYPE WINNER LIST>   ;"a TYPE with funny application"$
#FUNCTION ((W "TUPLE" T (!.W !.T))
<#WINNER (A B C) <+ 1 2> q>$
(A B C 3 q)

The following sequence makes MDL look just like Lisp. (This example is understandable only if you know Lisp (Moon, 1974); it is included only because it is so beautiful.)


So now:

(+ 1 2)$
(SET 'A 5)$

To complete the job, of course, we would have to do some SETG's: car is 1, cdr is ,REST, and lambda is ,FUNCTION. If you really do this example, you should "undo" it before continuing: