The pymdl book

16. Association (properties)

There is an "associative" data storage and retrieval system embedded in MDL which allows the construction of data structures with arbitrary selectors. It is used via the SUBRs described in this chapter.

13.1. Associative Storage

13.1.1. PUTPROP

<PUTPROP item:any indicator:any value:any>

("put property") returns item, having associated value with item under the indicator indicator.

13.1.2. PUT

<PUT item:any indicator:any value:any>

is identical to PUTPROP, except that, if item is structured and indicator is of TYPE FIX or OFFSET, it does <SETLOC <AT item indicator> value>. In other words, an element with an integral selector is stored in the structure itself, instead of in association space. PUT (like AT) will get an error if indicator is out of range; PUTPROP will not.

This is the whole of chapter 10's remark that PUT "is actually more general than this". A PUT with a FIX writes into the structure; a PUT with anything else writes into association space; and PUTPROP always does the latter, even with a FIX.

13.1.3. Removing Associations

If PUTPROP is used without its value argument, it removes any association existing between its item argument and its indicator argument. If an association did exist, using PUTPROP in this way returns the value which was associated. If no association existed, it returns #FALSE ().

PUT, with arguments which refer to association, can be used in the same way.

<PUTPROP A B 1>            ⇒ A
<GETPROP A B>              ⇒ 1
<PUTPROP A B>              ⇒ 1
<GETPROP A B>              ⇒ #FALSE ()
<GETPROP A B DEFAULT>      ⇒ DEFAULT
<PUTPROP A B>              ⇒ #FALSE ()

Note that PUTPROP with a value answers the item while PUTPROP without one answers the old value; the second is the only way to see what was there.

If either item or indicator ceases to exist (that is, no one was pointing to it, so it was garbage-collected), and no locatives to the association exist, then the association between them ceases to exist (is garbage-collected).

pymdl's own. That paragraph describes a weak reference, and it is the one thing in this chapter pymdl does not reproduce: the association table holds its item and indicator strongly, so an association survives until it is removed by hand, where the era's would have been collected along with its participants. Nothing a program can compute distinguishes the two -- an association you can still reach is one whose item you can still reach -- so the difference is in memory held, not in answers given. It matters only for a session that makes very many short-lived associations, and the remedy is the one the section already gives: two- argument PUTPROP removes one. Chapter 25 has where the era's collector would have done it for you.

13.2. Associative Retrieval

13.2.1. GETPROP

<GETPROP item:any indicator:any exp:any>

("get property") returns the value associated with item under indicator, if any. If there is no such association, GETPROP returns EVAL of exp (that is, exp gets EVALed both at call time and later).

exp is optional. If not given, GETPROP returns #FALSE () if it cannot return a value.

Note: item and indicator in GETPROP must be the same MDL objects used to establish the association; that is, they must be ==? to the objects used by PUTPROP or PUT.

13.2.2. GET

<GET item:any indicator:any exp:any>

is the inverse of PUT, using NTH or GETPROP depending on the test outlined in section 13.1.2. exp is optional and used as in GETPROP.

13.3. Examples of Association

<SET L '(1 2 3 4)>                 ⇒ (1 2 3 4)
<PUT .L FOO "L is a list.">        ⇒ (1 2 3 4)
<GET .L FOO>                       ⇒ "L is a list."
<PUTPROP .L 3 '![4]>               ⇒ (1 2 3 4)
<GETPROP .L 3>                     ⇒ ![4!]
<GET .L 3>                         ⇒ 3
<SET N 0>                          ⇒ 0
<PUT .N .L "list on a zero">       ⇒ 0
<GET .N '(1 2 3 4)>                ⇒ #FALSE ()

The PUTPROP/GETPROP pair in the middle is the distinction of 13.1.2 shown directly: <PUTPROP .L 3 ...> associated a UVECTOR with the LIST under the indicator 3 without touching the LIST, so <GETPROP .L 3> finds it while <GET .L 3> still answers the third element.

The last example failed because READ generated a new LIST -- not the one which is L's LVAL. However,

<SET L '(1 2 3 4)>                 ⇒ (1 2 3 4)
<SET N 0>                          ⇒ 0
<PUT .N .L "list on a zero">       ⇒ 0
<GET 0 .L>                         ⇒ "list on a zero"

works because <==? .N 0> is true.

That pair is section 8.2.2 doing real work. Two FIXes of the same value are the same object, so 0 finds it; two LISTs that print alike are not, so a freshly read (1 2 3 4) does not. It is the single most common way to be confused by this chapter.

To associate something with the Nth position in a structure, as opposed to its Nth element, associate it with <REST structure N-1>, as in the following:

<SET L '(1 2 3 4)>                 ⇒ (1 2 3 4)
<PUT <REST .L 2> PERCENT 0.3>      ⇒ (3 4)
<GET <2 .L> PERCENT>               ⇒ #FALSE ()
<GET <REST .L 2> PERCENT>          ⇒ 0.30000000

The manual writes that PUT as <PUT <REST .L 3> PERCENT 0.3>, which contradicts both its own answer (3 4) and the rule stated just above it: the third position is <REST structure 2>. With the 2 every line matches.

Why this works at all is chapter 10's sharing rule again. <REST .L 2> is not a copy, so the same call later produces an object that is ==? to the one the association was made with, while <2 .L> is the element 3 and has nothing to do with the position.

Remember comments?

<SET N '![A B C ;"third element" D E]>     ⇒ ![A B C D E!]
<GET <REST .N 2> COMMENT>                  ⇒ "third element"

The ' in the <SET N ...> is to keep EVAL from generating a new UVECTOR ("Direct Representation"), which would not have the comment on it (and which would be a needless duplicate).

pymdl's own. This is where chapter 5's promise about ; comments is paid off, and it is worth appreciating how strange it is: a comment is not discarded by READ, and it is not stored in the structure either. It is an ordinary association, on the position it followed, under the indicator COMMENT -- so GET retrieves it with no special machinery, and a program can attach its own annotations the same way. pymdl's reader does exactly this, which is why the example above works and why re-EVALuating the UVECTOR would lose it.

A "top-level" comment -- one attached to the entire object returned by READ -- is PUT on the CHANNEL in use, since there is no position in any structure for it. If no top-level comment follows the object, READ removes the value (<PUT channel COMMENT>); so anybody that wants to see a top-level comment must look for it after each READ.

If you need to have a structure with selectors in more than one dimension (for example, a sparse matrix that does not deserve to be linearized), associations can be cascaded to achieve the desired result. In effect an extra level of indirection maps two indicators into one. For example, to associate value with item under indicator-1 and indicator-2 simultaneously:

<PUTPROP INDICATOR-1 INDICATOR-2 T>                        ⇒ INDICATOR-1
<PUTPROP ITEM <GETPL INDICATOR-1 INDICATOR-2> 42>          ⇒ ITEM
<GETPROP ITEM <GETPL INDICATOR-1 INDICATOR-2>>             ⇒ 42

The trick is that GETPL (section 12.1.4) returns a LOCAS -- a locative to the association just made -- and the same LOCAS comes back on the second call, so it serves as a single indicator standing for the pair.

13.4. Examining Associations

Associations (created by PUT and PUTPROP) are chained together in a doubly-linked list, internal to MDL. The order of associations in the chain is their order of creation, newest first. There are several SUBRs for examining the chain of associations. ASSOCIATIONS returns the first association in the chain, or #FALSE () if there are none. NEXT takes an association as an argument and returns the next association in the chain, or #FALSE () if there are no more. ITEM, INDICATOR and AVALUE all take an association as an argument and return the item, indicator and value, respectively. Associations print as:

#ASOC (item indicator value)

(sic: only one S).

<PUTPROP X Y 1>            ⇒ X
<TYPE <ASSOCIATIONS>>      ⇒ ASOC
<ASSOCIATIONS>             ⇒ #ASOC (X Y 1)
<ITEM <ASSOCIATIONS>>      ⇒ X
<INDICATOR <ASSOCIATIONS>>     ⇒ Y
<AVALUE <ASSOCIATIONS>>    ⇒ 1
<PUTPROP P Q 2>            ⇒ P
<ITEM <ASSOCIATIONS>>      ⇒ P
<ITEM <NEXT <ASSOCIATIONS>>>   ⇒ X

"Newest first" is the last three lines: P was associated after X, and it is P that ASSOCIATIONS hands back.

Example: the following gathers all the existing associations into a LIST.

<PROG ((A <ASSOCIATIONS>))
 <COND (<NOT .A> '())
       (T (.A !<MAPF ,LIST
                <FUNCTION () <COND (<SET A <NEXT .A>> .A)
                                   (T <MAPSTOP>)>>>))>>

Found along the way. That PROG is shown rather than run, and the reason is a small surprise: <ASSOCIATIONS> in a fresh pymdl is not #FALSE (). It answers #ASOC (BLOCKED!-INTERRUPTS INTERRUPT #IHEADER BLOCKED!-INTERRUPTS), because the interrupt system of chapter 24 keeps its own handler table in association space, exactly as the era's does -- an interrupt's handlers are associated with its name under the indicator INTERRUPT. So the chain is never empty, the PROG above would list the interpreter's own furniture along with yours, and the !-INTERRUPTS trailer on that ATOM is chapter 18's oblist notation.

It is a good illustration of the chapter's opening claim. Association space is not a side facility bolted on for user programs; the interpreter builds its own tables out of it, and so does the COMMENT machinery two sections up.