|
|
|
|
home / intro / faq / tutorial / manual / models / download / people / about |
||
| naming objects |
|
Printing using object constructor syntax raises a problem. When the identity of an object depends on others, a bulky expression results. For instance, the following code shows a SPECIES object that has two identity fields which contain a SPECIES-TYPE object and a COMPARTMENT object:
| [species [species-type 'glucose] [compartment 'cell]] |
Deciding when too much information is being displayed depends on the needs of the user. For instance, from the perspective of a little b programmer, this code effectively communicates the relationship between several types of computational objects: three concept instances of type species, species-type and compartment, and two symbols, GLUCOSE and CELL - all this with a minimum of syntax getting in the way. However, from the perspective of a biologist, it buries the meaning - glucose exists in cell - under a mountain of type information and square-brackets.
The print eval identitical principle states that objects print out as code which accesses that object. In little b, a naming is a way to provide advice to the printing system so alternate code can be printed. The value of this code must be the object itself. That is, evaluating the code represented by a name must returns the object.
symbols In the example below (left), DEFVAR is used to define a special variable which holds the object [x 1]. The expression {x-one :# [x 1]} uses the naming operator (:#) to tell the printing system that the symbol X-ONE is a name for [x 1]. The DEFINE macro performs naming and assignment, and ensures that the symbol associated with the object cannot be changed (example on right). Currently, the naming operator only stores the first name given to an object.
| symbols as names | a better approach | |
|
B-USER 1> (defcon x ()
(a)) {x :# #<concept-class 04C398BE>} B-USER 2> (defvar x-one [x 1]) (new) [x 1] X-ONE B-USER 3> {x-one :# [x 1]} ; assign name {x-one :# [x 1]} B-USER 4> x-one {x-one :# [x 1]} B-USER 5> [x 1] {x-one :# [x 1]} B-USER 6> [x [x 1]] ; observe only name is printed (new) [x x-one] [x x-one] |
B-USER 1> (defcon x () (a)) {x :# #<concept-class 04C398BE>} B-USER 2> (define x-one [x 1]) ; name & assign in one step (new) {x-one :# [x 1]} X-ONE B-USER 3> {x-one := new-value} ; x-one may not be set Error: Attempt to set global lexical constant X-ONE. 1 (abort) Return to level 0. 2 Return to top loop level 0. Type :b for backtrace, :c <option number> to proceed, or :? for other options B-USER 4 : 1 > :top B-USER 5 > | |
printing context Context affects how names are printed. When the object is being printed inside another object, only the name is printed; otherwise, both the name and the constructor syntax are displayed. For example, notice after [x 1] is named (line 3) and subsequently printed by the REPL (lines 3-5), the name and constructor are displayed. I.e., as {x-one :# [x 1]}. However, when the object is printed out as part of another object (line 6), only the name is displayed: [x x-one] rather than [x [x 1]].
Name printing can be turned off entirely by setting the variable *DEBUG-PRINTING* to T, or to a type expression. E.g., {*DEBUG-PRINTING* := '(OR X Y)} turns off name printing for objects of type X or Y.
property fields Fields can also be used as names. In the example below, we add a property, NEXT to class X, and use this property to name other instances of x. The generated names have the form OBJECT.FIELD. The OBJECT itself may be named, either with a symbol or a field of yet another object. So, names can end up looking like: SYMBOL-NAME.FIELD.FIELD.FIELD...
On line 11, for example, [x 3] is named as the NEXT field of [x 2]. Since [x 2] is named x-one.next, [x 3]'s name is x-one.next.next. Lines 9 and 11 show examples of using the assignment operator (:=) followed by naming operator (:#) to assign names to objects. The naming and assignment (:#=) is a convenience which combines these two operations.
| property fields as names | |||||||||
|
B-USER 7> (defprop x.next
()) X.NEXT B-USER 8> {x-one.next := [x 2]} ; store [x 2] in [x 1].next (new) [x 2] [x 2] B-USER 9> {x-one.next :# [x 2]} ; record x-one.next as a name for [x 2] {x-one.next :# [x 2]} B-USER 10> {[x 2].next := [x 3]} ; store [x 3] in [x 2].next (new) [x 3] [x 3] B-USER 11> {[x 2].next :# [x 3]} ; notice the name which results {x-one.next.next :# [x 3]} B-USER 12> {[x 3].next :#= [x 4]} ; shorthand: naming & assignment at once (new) {x-one.next.next.next :# [x 4]} {x-one.next.next.next :# [x 4]} |
|||||||||
method fields Method fields can also act as names. The example below shows how to automate the x.next naming scheme using a method field.
Method fields with arguments can also be used. In this case, the name will have the form OBJECT.(FIELD ARG1 ARG2...). Mixing symbol, method field and property field names gives expressions like SYMBOL.FIELD.(FIELD ARG1 ARG2).FIELD...
| method fields as names | |||||||||
|
B-USER 7> (defield x.next
() {object.next :# [x {1 + object.a}]}) X.NEXT B-USER 8> x-one.next ; create [x 2] (new) {x-one.next :# [x 2]} {x-one.next :# [x 2]} B-USER 9> x-one.next.next.next ; etc (new) {x-one.next.next :# [x 3]} (new) {x-one.next.next.next :# [x 4]} {x-one.next.next.next :# [x 4]} |
|||||||||
summary This section summarizes the three forms which generate and assign names: the core naming operator (:#), the naming and assignment operator, (:#=) and the DEFINE macro.
| form | description | example |
|
:# the naming operator |
A binary operator of the form {name-form :# object-form} which generates a name from name-form, evaluates object-form in a context where *NAME* is bound to the name, and assigns the name to the object. | {x-one :# [x 1]} |
|
:#= the naming and assignment operator |
A binary operator of the form {name-place-form :#= object-form} which generates the name as for :# above and assigns the object to the name-place as by (SETF name-place-form object). | {x-one.next :#= [x 2]} |
|
DEFINE macro |
A macro of 2 required and one optional argument: (DEFINE name-place-form object-form [doc-string]). Conceptually similar to :#=, it first defines a global variable if name-place-form is a symbol. Detailed semantics are in sections below. | (define x-one [x 1] "The first x object.") |
| advanced material |
matching patterns
|
The remaining sections cover techniques and implementation details relating to names. You may choose to skip to the next section.
using names for identity Names, besides being used as advice to the printing system, may be treated as data. Using names for object identity is a useful technique for automatically generating symbolic identifiers. A name computed from the LHS of the naming operator is bound to a special variable, *NAME*. It is in this context that the RHS is evaluated. This makes it possible to treat the name object, a data structure, in a field.
In the code below, objects are constructed by using a name for the identity field. On line 2, the object constructor [y *NAME*] explicitly references the *NAME* variable, and captures the current name. Currently, names are implemented data structures (of type NAME). Names are created using a unary prefix operator, as in {_name name-form}. Names print out using this name operator syntax. When you see {_name X}, you are looking at a print-eval-identitical representation of a name data structure based on the name X.
User code should not depend on the details of representation of the name structure, as this is expected to change. Future versions may implement names as symbols and lists rather than as data structures.
| names as values | |||||||||
|
B-USER 1> (defcon y ()
(a)) {y :# #<concept-class 05FEC5D2>} B-USER 2> (define y-obj [y *name*]) (new) {y-obj :# [y {_name y-obj}] {y-obj :# [y {_name y-obj}]} B-USER 3> (defcon z () (&optional (a := *name*))) {z :# #<concept-class 023AB900>} B-USER 4> (define z-obj [z]) (new) {z-obj :# [z {_name z-obj}]} {z-obj :# [z {_name z-obj}]} B-USER 5> (defprop y.p ()) Y.P B-USER 6> {y-obj.p :#= [z]} (new) {y-obj.p :# [z {_name y-obj.p}]} {y-obj.p :# [z {_name y-obj.p}] |
|||||||||
define The DEFINE macro performs both naming and assignment (like :#=). It takes 3 arguments: a name, a form which is evaluated, and an optional documentation string. It is intended for generating names based on globally accessible symbols. Conceptually, what it does is similar to the code shown in symbols as names, that is an object is assigned to a variable (a symbol), and the symbol is used as the name of the object. There is one important difference, however.
DEFINE generates what Lispers sometimes call a "global lexical". There are some technical details here which we will gloss over. A discussion is presented below. The main point to understand is that (DEFINE name object) is similar in meaning to:
| (DEFVAR name object) {name :# object} |
define makes global lexical names Where DEFINE differs from the DEFVAR code in symbols as names is a subtle point, which relates to the behavior of Lisp special variables. DEFINE attempts to address issues that arise when attempting to use special variables in a context where the user needs to define a large number of global constants. Lisp special variables were not designed with this purpose in mind.
There are two kinds of special variables: dynamic (declared with DEFVAR or DEFPARAMETER) and constant (declared with DEFCONSTANT). Both kinds have undesirable interactions with variable-binding forms, like LET, LET*, etc, for our purposes. Specifically, symbols declared constant may not be bound as variables in LET forms (an error results).
As a result, we would expect errors to result from inadvertent reuse of a constant symbol, for example in code written by two different authors. The way Lispers get around this issue is by use of a naming convention: constant symbols are marked with + characters, +LIKE-THIS+. This is unacceptable for modular models which are expected to have a large number of global constants - both because of the visual clutter that would result, and the likelihood that many users simply would not follow this convention out of ignorance or irritation.
Dynamic variables are also unacceptable since their values can be set (we want constants), but more insidiously, because they can be dynamically bound. Dynamic binding is a powerful technique for transmitting information about the current execution context. However, for us this means that code can inadvertently alter the meaning of a global symbol. Lispers avoid this kind of confusion by again adopting a naming convention. Dynamic variables are written with asterisks, *LIKE-THIS*. So, we have the problem as above.
DEFINE solves these problems by using Lisp's symbol-macro facility. Symbol macros are a way of replacing a symbol with arbitrary code at macro-expansion time, when the symbol is in an evaluation context. DEFINE stores objects in a hidden variable. The code below shows how this might be done: a special variable is created in the HIDDEN package. Here, X-ONE is a symbol-macro which expands to a function call, (HIDDEN::B-USER~X-ONE), which accesses the hidden variable. The function ensures that the global variable cannot be changed. An attempt to set X-ONE will result in an attempt to call the SETF accessor of the HIDDEN::B-USER~X-ONE function, which generates an error. (DEFINE X-ONE [x 1]) is equivalent to the following:
| (defvar HIDDEN::B-USER~X-ONE [x 1]) (defun HIDDEN::B-USER~X-ONE () ; fn to shield the variable from being read HIDDEN::B-USER~X-ONE) (defun (SETF HIDDEN::B-USER~X-ONE) (VALUE) (error "X-ONE is a constant and cannot be set")) (define-symbol-macro x-one (HIDDEN::B-USER~X-ONE) {X-ONE :# HIDDEN::B-USER~X-ONE} ; name [x 1] with symbol X-ONE |
Observe how this works. When the symbol X-ONE is evaluated, it is macroexpanded - since it has been defined as a symbol-macro - to the function call, (HIDDEN::B-USER~X-ONE). This function does nothing other than return the value of the variable, HIDDEN::B-USER~X-ONE. If you try to assign a value to X-ONE, macroexpansion - i.e., {X-ONE := ...} == (SETF (HIDDEN::B-USER~X-ONE) ...) - the SETF function is invoked, which generates an error. X-ONE is effectively constant.
The symbol macro allows X-ONE to be bound. However, the binding is only lexical. That is outside a textual block in which a binding is made, the global value of X-ONE is used. The example below shows how the value of a special variable changes as a result of the dynamic context.
| dynamic variables depend on execution context | global lexicals do not depend on execution context | |
|
(defvar x :global) (defun print-x () (prin1 x)) (defun do-something () (print-x)) (do-something) ; :GLOBAL is printed (let ((x :local)) (do-something)) ; :LOCAL is printed (do-something) ; :GLOBAL is printed |
(define x :global) (defun print-x () (prin1 x)) (defun do-something () (print-x)) (do-something) ; :GLOBAL is printed (let ((x :local)) (do-something)) ; :GLOBAL is printed (print-x-one) ; :GLOBAL is printed |
the naming procedure Names are generated and assigned to objects using the :# or :#= binary operators, or the DEFINE macro. Code above (properties as names, methods as names) shows examples of using the naming (:#) and naming & assignment (:#=) operators. The core functionality is contained in the naming operator. It takes left and right hand side arguments (LHS and RHS), and is referred to as a binary operator for this reason. On the LHS is a form which represents the name to be assigned to the object computed on the RHS:
Note that name assignment operates from left to right, not right to left, as for the assignment operator (:=). That is, the name is being assigned to the object, not the other way around. The print system maintains a table which allows it to find a name given an object. The naming and assignment operator (:#=) assigns the value from right to left and assigns the name from left to right.
The LHS of the naming operator, the name-form, may be a symbol or a list. When the name-form is a symbol, no further processing is required. The name is the symbol.
When the name-form is a list, the first element of the list must be a symbol which names a function. It may not be a macro. A new list is constructed where the rest of the elements of the list are replaced with their values:
(F A1...An) =N=> (F V1...Vn),
where F is a symbol naming a function,
and An, Vn are a Lisp form, and its value.
Conceptually, lists as names represent partially evaluated code - the arguments of the function call have been evaluated, but the call has not yet been made. The reason for this is to allow names to be generated on the fly by code. By determining values of the arguments, we ensure that the name values which do not depend on local variables which were used to generate the name. In the example below, the name (storage-value 1) is generated with a local variable, A, which is evaluated when the name is created:
representation of field names Since dotted expressions are converted to lists (called "field forms") by the reader (i.e., before the naming operator sees them), they are in effect treated like lists. Specifically,
O.(F A1...An)
=R=> (FLD O :F A1...An)
=N=> (FLD O :F V1...Vn)
where F, :F are a field name and corresponding keyword
and An, Vn are a Lisp form, and its value.
When the printing system encounters a list which starts with FLD, it treats it specially, printing it out using dot syntax.
| (defvar *storage* (list nil nil nil)) (defun stored-value (n) (nth *storage*)) (defun (setf stored-value) (value n) (setf (nth *storage*) value)) (let ((a 1)) {(stored-value a) := [x 1]} ; store [x 1] in location 1 {(stored-value a) :# [x 1]}) ; name [x 1] (stored-value 1) |
matching patterns
|