213: Identifier Properties

by Marc Nieper-Wißkirchen

Status

This SRFI is currently in final status. Here is an explanation of each status that a SRFI can hold. To provide input on this SRFI, please send email to srfi-213@nospamsrfi.schemers.org. To subscribe to the list, follow these instructions. You can access previous messages via the mailing list archive.

Abstract

Using the define-property definition described in this SRFI, expand-time properties can be associated with identifiers in a referentially transparent and lexically scoped way.

Rationale

In many dialects of Lisps, symbols have attached properties. Each property associates a key with a value. Traditionally, identifiers in Lisp are the same as symbols and traditional Lisp macros transform Lisp data (usually represented by s-expressions). In particular, symbol properties can be used by one macro to record information about an identifier for use by another dependent macro.

In contrast to traditional Lisps, Scheme has hygienic macros and lexical scoping. The hygiene condition necessitates that identifiers are in general no longer symbols, so we want to attach properties to identifiers instead of symbols. To achieve lexical scoping of these properties, the properties have, in fact, to be attached to the binding of an identifier and become expand-time properties. Moreover, to preserve referential transparency, a property of an identifier has to be associated with the binding of a key that is itself an identifier instead of a symbol.

An implementation of this idea of identifier properties is this SRFI, which describes the define-property definition, which attaches a property to an identifier, and a way for macro transformers to look up these properties.

Use cases are numerous. One could write a pattern matcher together with a template engine akin to the syntax-case/syntax pair so that properties are attached to identifiers that represent pattern variables for identification in the template elements. In fact, one could portably implement syntax-case and syntax on top of the low-level macro facility (srfi 211 low-level) with this SRFI. Another use case is the attachment of properties to record-type descriptors for, say, the use in child record type definitions. Or one could attach compile-time information to procedures for optimization purposes at their use site, and so on.

Example

In order to attach a property to an identifier binding, we have to create such a binding first:

    (define computer "the-computer")

Moreover, we need a binding that can act as the property's key:

    (define answer #f)

Attaching the property is then as simple as

    (define-property computer answer (* 2 21))

As the property associates an expand-time value, we have to retrieve the property through a macro:

    (define-syntax get-the-answer
      (lambda (stx)
        (capture-lookup
          (lambda (lookup)
            (syntax-case stx ()
              ((_ x key)
               (let ((res (lookup #'x #'key)))
                 (cond
                   ((not res) #f)
                   ((number? res) res)
                   (else
                    (error "not a number" res))))))))))

We then have the expansion (and thus evaluation) of

    (get-the-answer computer answer)   ⟹ 42

By the way, attaching the property hasn't disturbed the original binding:

    computer   ⟹ "the-computer"

If we shadow the binding of the key, we cannot access the property any more, making private interfaces possible:

    (let ((answer #f))
      (get-the-answer computer answer))   ⟹ #f

    (get-the-answer computer answer)   ⟹ 42

The same is true if we change the meaning of the identifier the property is attached to:

    (let ((computer "the-super-computer"))
      (get-the-answer computer answer))   ⟹ #f

    (get-the-answer computer answer)   ⟹ 42

Finally, the attachment of properties is lexically scoped:

    (let* ()
      (define-property computer answer 43)
      (get-the-answer computer answer))      ⟹ 43

    (get-the-answer computer answer)   ⟹ 42

Specification

Syntax

Like other definitions, the define-property definition can either appear at the outermost level or in a body where other definitions can appear.

(define-property identifier key expression)

Syntax: Identifier and key are bound identifiers. Expression can be any expression.

Semantics: The expression is evaluated at macro-expansion time and a new property associating the binding of key with the resulting value is attached to the binding of identifier without disturbing the existing meaning of the identifier in the scope of that binding.

The scope of a property introduced by define-property is the entire body in which the define-property form appears or the outermost level, except where it is replaced by a property for the same identifier and key or where the binding to which it is attached is shadowed.

Any number of properties can be attached to the same binding with different keys. Attaching a new property with the same name as a property already attached to a binding shadows the existing property with the new property.

Note: Contrary to other definitions, it is not an error to define the same property more than once in the same body.

Procedures

(capture-lookup proc)

Capture-lookup is used in macro transformers as described below. When invoked, it returns a single value.

It is an error if proc is not a procedure accepting a single argument.

For backward compatibility, capture-lookup must be substitutable by the identity (lambda (proc) proc) when used in macro transformers as described below.

Transformers

If a transformation procedure returns the result of invoking capture-lookup on a procedure proc instead of returning a syntax object when applied to the input of a macro use, the transformer is re-entered by invoking proc with the single argument lookup.

Lookup is a procedure accepting two identifier arguments id and key. The lookup procedure returns the value of the property of id associated with the binding of key or #f if there is no such property. It is an error if key is not bound. If id is unbound, an error is signaled.

Rationale: The reason why it is enforced that an error is signaled when id is unbound is so that macro transformers can test through the facilities provided by this SRFI whether identifiers are bound (in the transformer environment).

If an er-macro-transformer returns the result of invoking capture-lookup on a procedure proc instead of returning a datum when applied to the input of a macro use, the transformer is re-entered by invoking proc with the single argument lookup.

The lookup procedure is as in the case of transformer procedures except that if it is called with a symbol for the argument for id or key, the symbol behaves as if it represents an identifier introduced at the macro use.

If an ir-macro-transformer returns the result of invoking capture-lookup on a procedure proc instead of returning a datum when applied to the input of a macro use, the transformer is re-entered by invoking proc with the single argument lookup.

The lookup procedure is as in the case of transformer procedures except that if it is called with a symbol for the argument for id or key, the symbol behaves as if it represents an identifier introduced at the macro definition.

If a sc-macro-transformer returns the result of invoking capture-lookup on a procedure proc instead of a returning form when applied to the input of a macro use, the transformer is re-entered by invoking proc with the single argument lookup.

The lookup procedure is as in the case of transformer procedures except that it accepts four arguments id-env, id, key-env, and key, where id-env and key-env are syntactic environments. The bindings of id and key are then looked up in these environments.

If a lisp-transformer returns the result of invoking capture-lookup on a procedure proc instead of returning a datum when applied to the input of a macro use, the transformer is re-entered by invoking proc with the single argument lookup.

The lookup procedure is as in the case of transformer procedures except that it accepts symbols instead of identifiers as arguments for id or key. The symbols behave as if they represent identifiers introduced at the macro use.

Implementation

This SRFI is provided by the Unsyntax implementation of the Scheme programming language.

Chez Scheme implements define-property in its (chezscheme) library.

Acknowledgements

The author read about the define-property definition in the Chez Scheme User's Guide. Some of the wording was shamelessly stolen from the manual.

© 2020-2021 Marc Nieper-Wißkirchen.

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


Editor: Arthur A. Gleckler