DEFSTRUCT — 구조체 타입 정의하기

DEFSTRUCT — 구조체 타입 정의하기 (매크로)

이름 붙은 슬롯들을 가진 구조적 type(구조체)을 정의하는 매크로예요. 슬롯마다 reader 함수를 만들고 setf와 연동되게 하며, 기본적으로 predicate·constructor·copier 함수까지 자동 생성해요. 데이터 레코드를 깔끔하게 다루는 가장 기본적인 도구예요.

출처: DEFSTRUCT - Common Lisp HyperSpec

시그니처 (Syntax)

defstruct name-and-options [documentation] {slot-description}* => structure-name
name-and-options ::= structure-name | (structure-name [[options]])

options ::= conc-name-option |
            {constructor-option}* |
            copier-option |
            include-option |
            initial-offset-option |
            named-option |
            predicate-option |
            printer-option |
            type-option

conc-name-option ::= :conc-name | (:conc-name) | (:conc-name conc-name)
constructor-option ::= :constructor |
                       (:constructor) |
                       (:constructor constructor-name) |
                       (:constructor constructor-name constructor-arglist)
copier-option ::= :copier | (:copier) | (:copier copier-name)
predicate-option ::= :predicate | (:predicate) | (:predicate predicate-name)
include-option ::= (:include included-structure-name {slot-description}*)
printer-option ::= print-object-option | print-function-option
print-object-option ::= (:print-object printer-name) | (:print-object)
print-function-option ::= (:print-function printer-name) | (:print-function)
type-option ::= (:type type)
named-option ::= :named
initial-offset-option ::= (:initial-offset initial-offset)

slot-description ::= slot-name |
                     (slot-name [slot-initform [[slot-option]]])

slot-option ::= :type slot-type | :read-only slot-read-only-p
  • conc-namestring designator.
  • constructor-arglistboa lambda list.
  • constructor-namesymbol.
  • copier-namesymbol.
  • included-structure-name — 이미 정의된 structure name. derived typestructure name으로 확장되더라도 허용되지 않는다는 점을 유의하세요.
  • initial-offset — 음이 아닌 integer.
  • predicate-namesymbol.
  • printer-namefunction name 또는 lambda expression.
  • slot-namesymbol.
  • slot-initformform.
  • slot-read-only-pgeneralized boolean.
  • structure-namesymbol.
  • typetype specifier list, vector, (vector size) 중 하나, 또는 implementation이 적절하다고 정의한 다른 type specifier.
  • documentationstring. 평가되지 않아요.

본문 (Description)

defstructslot-option이 지정한 대로 이름 붙은 슬롯을 가진 structure-type이라는 구조적 type을 정의해요.

defstruct는 슬롯에 대한 reader를 정의하고, 그런 reader 함수에 setf가 제대로 동작하도록 해요. 덮어쓰지 않는 한 name-p라는 predicate, make-constructor-name이라는 constructor 함수, copy-constructor-name이라는 copier 함수를 정의해요. 자동 생성된 함수의 모든 이름이 자동으로 inline 선언될 수 있어요(implementation 재량).

documentation이 주어지면 structure-namestructure 종류의 documentation string으로 붙고, :type을 쓰지 않는 한 structure-nametype 종류로, 그리고 structure-name이 가리키는 classclass object에도 documentation string으로 붙어요.

defstructdefstruct가 만든 구조체의 인스턴스를 만드는 데 쓰는 constructor 함수를 정의해요. 기본 이름은 make-structure-name이에요. constructor 옵션의 인자로 이름을 주면 다른 이름을 쓸 수 있어요. nil은 constructor 함수를 만들지 않는다는 뜻이에요.

새 구조체 타입이 정의된 뒤에는 보통 그 타입의 constructor 함수로 인스턴스를 만들 수 있어요. constructor 함수 호출은 다음 형태예요:

(constructor-function-name
 slot-keyword-1 form-1
 slot-keyword-2 form-2
 ...)

constructor 함수의 인자는 모두 키워드 인자예요. 각 슬롯 키워드 인자는 구조체 슬롯의 이름과 대응하는 이름을 가진 keyword여야 해요. 모든 keywordform은 평가돼요. 슬롯이 이런 식으로 초기화되지 않으면, constructor 함수가 호출될 때 슬롯 설명의 slot-initform을 평가해서 초기화해요. slot-initform이 제공되지 않으면 값이 명시적으로 할당되기 전에 슬롯 값을 나중에 읽으려는 시도의 결과는 정의되지 않아요.

defstruct 컴포넌트에 제공된 각 slot-initform은, 제공되지 않은 컴포넌트에 대해 constructor 함수가 쓸 때마다 constructor 함수 호출 때마다 재평가돼요. slot-initform은 특정 구조체 인스턴스를 만드는 데 필요할 때만 평가돼요. 결코 필요하지 않으면, 슬롯의 type이 지정되어 있어도 타입 불일치 에러가 있을 수 없고, 이 경우 경고도 내보내지 않아야 해요. 예를 들어 다음 수열에서 마지막 호출만 에러예요.

(defstruct person (name 007 :type string))
(make-person :name "James")
(make-person)

마치 slot-initform이 constructor 함수의 keyword parameterinitialization form으로 쓰인 것과 같아요.

슬롯을 이름 짓는 symbolimplementation이 constructor 함수의 lambda variablename으로 쓰면 안 돼요. 그 symbol 중 하나 이상이 special로 선언되어 있거나 constant variable의 이름으로 정의되어 있을 수 있기 때문이에요. 슬롯 기본 초기화 form은 defstruct form 자체가 나타나는 lexical environment와 constructor 함수 호출이 나타나는 dynamic environment에서 평가돼요.

예를 들어 (gensym)이라는 form을 constructor 함수 호출에서든 defstruct의 기본 초기화 form으로든 초기화 form으로 쓰면, constructor 함수를 호출할 때마다 gensym을 한 번 호출해 새 symbol을 만들어요.

defstruct의 각 slot-description은 0개 이상의 slot-option을 지정할 수 있어요. slot-option은 keyword와 값의 쌍으로 이루어져요(값은 평가될 form이 아니라 값 그 자체예요). 예:

(defstruct ship
  (x-position 0.0 :type short-float)
  (y-position 0.0 :type short-float)
  (x-velocity 0.0 :type short-float)
  (y-velocity 0.0 :type short-float)
  (mass *default-ship-mass* :type short-float :read-only t))

이것은 각 슬롯이 항상 short float를 담고, 마지막 슬롯은 ship을 만든 뒤 바꿀 수 없다는 것을 지정해요.

사용 가능한 slot-option은:

:type type — 슬롯의 내용이 항상 type 타입임을 지정해요. 변수나 함수의 선언과 완전히 유사하며, reader 함수의 결과 타입을 선언하는 셈이에요. type을 슬롯 초기화 때 검사할지 할당 때 검사할지는 implementation-dependent해요. Type은 평가되지 않고 유효한 type specifier여야 해요.

:read-only xxtrue면 슬롯을 바꿀 수 없다고 지정해요. 항상 생성 시점에 공급된 값을 담아요. setf는 이 슬롯의 reader 함수를 받아들이지 않아요. xfalse면 이 slot-option은 효과가 없어요. X는 평가되지 않아요.

이 옵션이 false이거나 제공되지 않으면, 슬롯을 write하는 능력을 setf function으로 구현할지 setf expander로 구현할지는 implementation-dependent해요.

다음 키워드 옵션들은 defstruct와 함께 쓸 수 있어요. defstruct 옵션은 keyword이거나 해당 keyword와 인자들의 list일 수 있어요. keyword를 그 자체로 지정하는 것은 그 keyword만 있고 인자가 없는 list를 지정하는 것과 같아요. defstruct 옵션의 문법은 slot-option에 쓰는 쌍 문법과 달라요. 이 옵션의 어떤 부분도 평가되지 않아요.

:conc-name

reader(또는 access) 함수 이름에 자동으로 접두사를 붙이는 것을 제공해요. 기본 동작은 구조체의 모든 reader 함수 이름을 구조체 이름 뒤에 하이픈을 붙여 시작하는 거예요.

:conc-name은 대체 접두사를 제공해요. 하이픈을 구분자로 쓰려면 접두사의 일부로 넣어야 해요. :conc-name이 nil이거나 인자가 없으면 접두사를 쓰지 않고, reader 함수 이름이 슬롯 이름과 같아져요. non-nil 접두사가 주어지면 각 슬롯의 reader function 이름은 그 접두사와 슬롯 이름을 이어 붙이고, 결과 symboldefstruct form이 확장되는 시점에 현재인 package에 intern해서 만들어요.

:conc-name에 무엇을 주든, 접두사 없이 슬롯 이름과 일치하는 슬롯 키워드가 constructor 함수에 쓰인다는 점을 유의하세요. reader 함수 이름은 setf와 함께 쓰여요. 예:

(defstruct (door (:conc-name dr-)) knob-color width material) =>  DOOR

(setq my-door (make-door :knob-color 'red :width 5.0))
=>  #S(DOOR :KNOB-COLOR RED :WIDTH 5.0 :MATERIAL NIL)

(dr-width my-door) =>  5.0
(setf (dr-width my-door) 43.7) =>  43.7
(dr-width my-door) =>  43.7

:conc-name 옵션이 명시적으로 주어졌든 아니든, 생성된 reader(또는 accessor) 이름의 이름 충돌은 다음 규칙을 따르게 돼요: 슬롯 X1에 대한 reader 함수 R이 있고 다른 structure type S2(X2 슬롯에 대한 같은 이름 R의 reader 함수를 만들 것)에 상속되는 structure type S1에 대해, S2의 정의로는 R의 정의가 생성되지 않아요. 대신 R의 정의는 S1의 정의에서 상속돼요. (이 경우 X1과 X2가 다른 슬롯이면 implementation이 스타일 경고를 신호할 수 있어요.)

:constructor

이 옵션은 인자를 0개, 1개, 또는 2개 받아요. 인자가 하나 이상 제공되고 첫 인자가 nil이 아니면 그 인자는 constructor 함수의 이름을 지정하는 symbol이에요. 인자가 제공되지 않으면(또는 옵션 자체가 제공되지 않으면) constructor 이름은 "MAKE-" 문자열과 구조체 이름을 이어 붙이고, defstruct가 확장되는 시점에 현재인 package에 intern해서 만들어져요. 인자가 제공되고 nil이면 constructor 함수는 정의되지 않아요.

:constructor가 (:constructor name arglist)로 주어지면, 키워드 구동 constructor 함수 대신 defstruct는 "순위(positional)" constructor 함수를 정의하는데, 그 인자들의 의미는 인자 위치와 가능한 키워드로 결정돼요. Arglist는 constructor의 인자가 무엇인지 설명하는 데 쓰여요. 가장 단순한 경우 (:constructor make-foo (a b c))는 make-foo를 a, b, c라는 슬롯을 초기화하는 데 쓰는 세 인자 constructor 함수로 정의해요.

이런 종류의 constructor는 "By Order of Arguments"로 동작해서 때로 "boa constructor"라고도 불러요.

"boa constructor"의 arglist가 처리되는 방법은 Section 3.4.6 (Boa Lambda Lists)를 보세요.

:constructor 옵션을 두 번 이상 쓰는 것이 허용돼요. 각각 다른 매개변수를 받는 여러 constructor 함수를 정의할 수 있으니까요.

defstruct는 명시적 :constructor 옵션이 하나도 지정되지 않았거나, :constructor 옵션이 name 인자 없이 지정된 경우에만 기본 이름의 키워드 constructor 함수를 만들어요.

(:constructor nil)은 다른 :constructor 옵션이 없을 때만 의미가 있고, defstruct가 어떤 constructor도 만들지 못하게 해요.

그 외에는 defstruct가 공급된 각 :constructor 옵션에 대응하는 constructor 함수를 만들어요. 여러 키워드 constructor 함수와 여러 "boa constructor"를 지정하는 것도 허용돼요.

:copier

이 옵션은 인자 하나(symbol)를 받아 copier 함수의 이름을 지정해요. 인자가 제공되지 않거나 옵션 자체가 제공되지 않으면 copier 이름은 "COPY-" 문자열과 구조체 이름을 이어 붙이고, defstruct가 확장되는 시점에 현재인 package에 intern해서 만들어져요. 인자가 제공되고 nil이면 copier 함수는 정의되지 않아요.

자동 정의된 copier 함수는 한 argument를 받는 함수이며, 그 인자는 정의되는 구조체 타입이어야 해요. copier 함수는 인자와 같은 type이고 원래 구조체와 같은 컴포넌트 값을 가진 fresh 구조체를 만들어요. 즉 컴포넌트 값은 재귀적으로 복사되지 않아요. defstruct :type 옵션을 쓰지 않았다면 다음 등식이 성립해요:

(copier-name x) = (copy-structure (the structure-name x))

:include

이 옵션은 새 구조체 정의를 다른 구조체 정의의 확장으로 만드는 데 쓰여요. 예:

(defstruct person name age sex)

name, age, sex 속성과 person 구조체에 동작하는 function을 가진, astronaut를 표현하는 새 구조체를 만들려면 :include로 astronaut를 이렇게 정의해요:

(defstruct (astronaut (:include person)
                      (:conc-name astro-))
   helmet-size
   (favorite-beverage 'tang))

:include는 정의되는 구조체가 포함된 구조체(included structure)와 같은 슬롯을 갖게 해요. 포함된 구조체의 reader 함수가 정의되는 구조체에도 동작하는 방식으로 이루어져요. 이 예에서 astronaut는 슬롯이 다섯 개예요: person에서 정의한 세 개와 astronaut 자신이 정의한 두 개. person 구조체가 정의한 reader 함수는 astronaut 구조체의 인스턴스에 적용될 수 있고 올바르게 동작해요. 게다가 astronaut는 person 구조체가 정의한 컴포넌트에 대한 자체 reader 함수도 가져요. 다음 예는 astronaut 구조체 사용을 보여줘요:

(setq x (make-astronaut :name 'buzz
                        :age 45.
                        :sex t
                        :helmet-size 17.5))
(person-name x) =>  BUZZ
(astro-name x) =>  BUZZ
(astro-favorite-beverage x) =>  TANG

(reduce #'+ astros :key #'person-age) ; obtains the total of the ages
                                      ; of the possibly empty
                                      ; sequence of astros

reader 함수 person-name과 astro-name의 차이는, person-name은 astronaut를 포함한 어떤 person에도 올바르게 적용될 수 있지만 astro-name은 astronaut에만 올바르게 적용된다는 점이에요. implementationreader 함수의 잘못된 사용을 검사할 수도 있어요.

단일 defstruct에는 :include가 최대 하나 제공될 수 있어요. :include의 인자는 필수이고, 이전에 정의된 어떤 구조체의 이름이어야 해요. 정의되는 구조체에 :type 옵션이 없으면 포함된 구조체에도 :type 옵션이 없었어야 해요. 정의되는 구조체에 :type 옵션이 있으면 포함된 구조체도 같은 표현 type을 지정하는 :type 옵션으로 선언되었어야 해요.

:type 옵션이 관여하지 않으면, 포함하는 구조체 정의의 구조체 이름은 data type의 이름이 되어 typep가 인식하는 유효한 type specifier가 돼요. 포함된 구조체의 subtype이 되죠. 위 예에서 astronaut는 person의 subtype이므로:

(typep (make-astronaut) 'person) =>  true

이것은 person에 대한 모든 연산이 astronaut에도 동작함을 나타내요.

:include를 쓰는 구조체는, :include 옵션을 이렇게 줘서 포함된 슬롯에 포함된 구조체가 지정한 것과 다른 기본값이나 slot-option을 지정할 수 있어요:

(:include included-structure-name slot-description*)

slot-description은 포함된 구조체의 어떤 슬롯과 같은 slot-name을 가져야 해요. slot-descriptionslot-initform이 없으면 새 구조체에서 그 슬롯은 초기값이 없어요. 그 외에는 초기값 form이 slot-descriptionslot-initform으로 교체돼요. 보통 쓰기 가능한 슬롯을 read-only로 만들 수도 있어요. 슬롯이 포함된 구조체에서 read-only면 포함하는 구조체에서도 반드시 read-only여야 해요. 슬롯에 type이 제공되면 포함된 구조체에서 지정한 typesubtype이어야 해요.

예를 들어 astronaut의 기본 나이가 45라면:

(defstruct (astronaut (:include person (age 45)))
   helmet-size
   (favorite-beverage 'tang))

:include를 :type 옵션과 함께 쓰면, 효과는 먼저 포함된 구조체를 표현하는 데 필요한 만큼 표현 요소를 건너뛰고, 그 다음 :initial-offset 옵션이 제공한 추가 요소를 건너뛴 뒤, 그 지점부터 요소 할당을 시작하는 거예요. 예:

(defstruct (binop (:type list) :named (:initial-offset 2))
   (operator '? :type symbol)
   operand-1
   operand-2) =>  BINOP

(defstruct (annotated-binop (:type list)
                            (:initial-offset 3)
                            (:include binop))
 commutative associative identity) =>  ANNOTATED-BINOP

(make-annotated-binop :operator '*
                      :operand-1 'x
                      :operand-2 5
                      :commutative t
                      :associative t
                      :identity 1)
  =>  (NIL NIL BINOP * X 5 NIL NIL NIL T T 1)

처음 두 nil 요소는 binop 정의의 :initial-offset 2에서 비롯돼요. 다음 네 요소는 binop의 구조체 이름과 세 슬롯을 담아요. 다음 세 nil 요소는 annotated-binop 정의의 :initial-offset 3에서 비롯돼요. 마지막 세 list 요소는 annotated-binop의 추가 슬롯을 담아요.

:initial-offset

:initial-offset는 defstruct가 본문에 설명된 슬롯 할당을 시작하기 전에 특정 수의 슬롯을 건너뛰도록 지시해요. 이 옵션의 인자는 defstruct가 건너뛰어야 할 슬롯 수예요. :initial-offset은 :type도 함께 제공될 때만 쓸 수 있어요.

:initial-offset는 슬롯이 첫 번째가 아닌 표현 요소부터 할당되게 해요. 예를 들어

(defstruct (binop (:type list) (:initial-offset 2))
  (operator '? :type symbol)
  operand-1
  operand-2) =>  BINOP

는 make-binop에 대해 다음 동작을 일으켜요:

(make-binop :operator '+ :operand-1 'x :operand-2 5)
=>  (NIL NIL + X 5)

(make-binop :operand-2 4 :operator '*)
=>  (NIL NIL * NIL 4)

선택 함수 binop-operator, binop-operand-1, binop-operand-2는 각각 third, fourth, fifth와 본질적으로 동등해요. 마찬가지로

(defstruct (binop (:type list) :named (:initial-offset 2))
  (operator '? :type symbol)
  operand-1
  operand-2) =>  BINOP

는 make-binop에 대해 다음 동작을 일으켜요:

(make-binop :operator '+ :operand-1 'x :operand-2 5) =>  (NIL NIL BINOP + X 5)
(make-binop :operand-2 4 :operator '*) =>  (NIL NIL BINOP * NIL 4)

처음 두 nil 요소는 binop 정의의 :initial-offset 2에서 비롯돼요. 다음 네 요소는 binop의 구조체 이름과 세 슬롯을 담아요.

:named

:named는 구조체가 이름 붙었음(named)을 지정해요. :type이 제공되지 않으면 구조체는 항상 이름 붙어요.

예:

(defstruct (binop (:type list))
  (operator '? :type symbol)
  operand-1
  operand-2) =>  BINOP

이것은 make-binop constructor 함수와 binop-operator, binop-operand-1, binop-operand-2 세 개의 선택 함수를 정의해요. (다만 이유가 있어서 predicate binop-p는 정의하지 않아요.)

make-binop의 효과는 단순히 길이 3의 list를 만드는 거예요:

(make-binop :operator '+ :operand-1 'x :operand-2 5) =>  (+ X 5)
(make-binop :operand-2 4 :operator '*) =>  (* NIL 4)

이건 keyword 인자를 받고 binop 개념적 데이터 타입에 맞는 슬롯 기본값을 수행한다는 점만 빼면 list 함수와 같아요. 마찬가지로 binop-operator, binop-operand-1, binop-operand-2 선택 함수는 각각 car, cadr, caddr와 본질적으로 동등해요. 완전히 동등하지는 않을 수 있는데, 예를 들어 구현이 각 선택 함수의 인자가 길이 3의 list인지 확인하는 에러 검사 코드를 추가하는 것도 타당하기 때문이에요.

binop은 개념적 데이터 타입으로, Common Lisp 타입 시스템의 일부가 되지 않아요. typep는 binop을 type specifier로 인식하지 않고, type-of는 binop 구조체에 list를 돌려줘요. make-binop이 만든 데이터 구조체를 우연히 올바른 구조를 가진 다른 list와 구분할 방법이 없어요.

make-binop이 만든 구조체에서 구조체 이름 binop을 되찾을 방법도 없어요. 이것은 구조체가 이름 붙었을 때만 가능해요. 이름 붙은 구조체는 구조체 인스턴스가 주어지면 (타입을 가리키는) 구조체 이름을 안정적으로 되찾을 수 있다는 성질을 가져요. :type 옵션 없이 정의된 구조체의 경우 구조체 이름이 실제로 Common Lisp 데이터 타입 시스템의 일부가 돼요. type-of는 그런 구조체에 적용되면 objecttype으로 구조체 이름을 돌려주고, typep는 구조체 이름을 유효한 type specifier로 인식해요.

:type 옵션으로 정의된 구조체의 경우 type-of는 :type 옵션에 준 타입에 따라 list나 (vector t) 같은 type specifier를 돌려줘요. 구조체 이름은 유효한 type specifier가 되지 않아요. 다만 :named 옵션도 제공되면 ( defstruct constructor 함수가 만든) 구조체의 첫 컴포넌트가 항상 구조체 이름을 담아요. 이로써 구조체 인스턴스에서 구조체 이름을 되찾을 수 있고, 개념적 타입에 대한 합리적인 predicate를 정의할 수 있어요: 자동 정의된 name-p predicate는 먼저 인자가 올바른 타입(list, (vector t), 등)인지 확인하고, 그다음 첫 컴포넌트에 적절한 타입 이름이 들어 있는지 검사하는 식으로 동작해요.

위 binop 예를 :named 옵션만 추가해 수정한 것을 생각해보세요:

(defstruct (binop (:type list) :named)
  (operator '? :type symbol)
  operand-1
  operand-2) =>  BINOP

이전과 마찬가지로 make-binop constructor 함수와 binop-operator, binop-operand-1, binop-operand-2 세 선택 함수를 정의해요. binop-p predicate도 정의해요. make-binop의 효과는 이제 길이 4의 list를 만드는 거예요:

(make-binop :operator '+ :operand-1 'x :operand-2 5) =>  (BINOP + X 5)
(make-binop :operand-2 4 :operator '*) =>  (BINOP * NIL 4)

구조체 이름 binop이 첫 list 요소로 포함된 것만 빼면 구조체는 이전과 같은 배치를 가져요. 선택 함수 binop-operator, binop-operand-1, binop-operand-2는 각각 cadr, caddr, cadddr와 본질적으로 동등해요. binop-p predicate는 다음 정의와 대략 동등해요:

(defun binop-p (x)
  (and (consp x) (eq (car x) 'binop))) =>  BINOP-P

name binop은 여전히 typep가 인식하는 유효한 type specifier가 아니지만, 적어도 binop 구조체를 다른 비슷하게 정의된 구조체와 구분하는 방법은 있어요.

:predicate

이 옵션은 인자 하나를 받아 타입 predicate의 이름을 지정해요. 인자가 제공되지 않거나 옵션 자체가 제공되지 않으면 predicate 이름은 구조체 이름 뒤에 "-P" 문자열을 붙이고, defstruct가 확장되는 시점에 현재인 package에 intern해서 만들어져요. 인자가 제공되고 nil이면 predicate는 정의되지 않아요. predicate는 구조체가 이름 붙었을 때만 정의할 수 있어요. :type이 제공되고 :named가 제공되지 않으면 :predicate는 제공되지 않거나 값이 nil이어야 해요.

:print-function과 :print-object 옵션은 structure-name 타입의 structure에 대한 print-object method가 생성되도록 지정해요. 이 옵션들은 동의어가 아니지만 비슷한 일을 수행해요. 어느 옵션(:print-function 또는 :print-object)을 쓰는지에 따라 printer-name이라는 함수가 호출되는 방식이 달라져요. 이 옵션 중 하나만 쓸 수 있고, :type이 제공되지 않을 때만 쓸 수 있어요.

:print-function 옵션을 쓰면, structure-name 타입 구조체를 출력할 때 지정된 printer 함수가 세 argument로 호출돼요:

  • 출력할 구조체(structure-namegeneralized instance).
  • 출력할 stream.
  • 현재 깊이를 나타내는 integer. 이 정수의 크기는 implementation마다 다를 수 있지만, 깊이 축약이 적절한지 판단하기 위해 print-level과 안정적으로 비교할 수 있어요.

(:print-function printer-name)을 지정하는 것은 다음을 지정하는 것과 대략 동등해요:

(defmethod print-object ((object structure-name) stream)
  (funcall (function printer-name) object stream <<current-print-depth>>))

여기서 <<current-print-depth>>는 printer가 현재 얼마나 깊이 출력 중인지에 대한 믿음을 나타내요. <<current-print-depth>>가 항상 0이고 print-levelnon-nil이면 출력이 재귀적으로 내려가면서 점점 작은 값으로 다시 bound되는지, 아니면 current-print-depth가 출력이 재귀적으로 내려가면서 값이 변하고 print-level은 같은 순회에서 일정하게 유지되는지는 implementation-dependent해요.

:print-object 옵션을 쓰면, structure-name 타입 구조체를 출력할 때 지정된 printer 함수가 두 인자로 호출돼요:

  • 출력할 구조체.
  • 출력할 stream.

(:print-object printer-name)을 지정하는 것은 다음을 지정하는 것과 동등해요:

(defmethod print-object ((object structure-name) stream)
  (funcall (function printer-name) object stream))

:type 옵션이 제공되지 않고 :print-function 또는 :print-object 옵션 중 하나가 제공되고 printer-name이 제공되지 않으면, #S 표기법을 쓰는 구조체의 기본 출력 동작을 구현하는 함수를 호출하는 structure-name에 대해 specializedprint-object method가 생성돼요. Section 22.1.3.12 (Printing Structures)를 보세요.

:print-function도 :print-object도 제공되지 않으면 defstructstructure-name에 대해 specializedprint-object method를 생성하지 않고, :include 옵션에 이름 붙은 구조체에서 상속되거나 구조체 출력의 기본 동작에서 어떤 기본 동작을 상속해요. function print-object와 Section 22.1.3.12 (Printing Structures)를 보세요.

print-circletrue일 때, 사용자 정의 print 함수는 write, prin1, princ, format을 써서 제공된 streamobject를 출력하고 순환 구조가 감지되어 #n# 문법으로 출력되길 기대할 수 있어요. 이것은 :print-function 옵션뿐 아니라 print-objectmethod에도 적용돼요. 사용자 정의 print 함수가 제공된 stream이 아닌 다른 stream에 출력하면, 그 stream에 대해 순환 감지가 다시 시작돼요. variable print-circle을 보세요.

:type

:type은 구조체에 쓰일 표현(representation)을 명시적으로 지정해요. 그 인자는 다음 type 중 하나여야 해요:

vector — (vector t)를 지정한 것과 같은 결과를 내요. 구조체는 일반 vector로 표현되고 컴포넌트를 vector 요소로 저장해요. 구조체가 :named이면 첫 컴포넌트는 vector 요소 1에, 그렇지 않으면 요소 0에 있어요.

(vector element-type) — 구조체는 (가능하면 특화된) vector로 표현되고 컴포넌트를 vector 요소로 저장해요. 모든 컴포넌트는 지정된 typevector에 저장될 수 있는 type이어야 해요. 구조체가 :named이면 첫 컴포넌트는 vector 요소 1에, 그렇지 않으면 요소 0에 있어요. 구조체는 type symbol이 제공된 element-typesubtype일 때만 :named일 수 있어요.

list — 구조체는 list로 표현돼요. 구조체가 :named이면 첫 컴포넌트는 cadr에, 아니면 car에 있어요.

이 옵션을 지정하면 특정 표현을 강제하고 컴포넌트가 defstruct에 지정된 순서대로 지정된 표현의 대응하는 연속 요소에 저장되게 해요. 또한 구조체 이름이 typep가 인식하는 유효한 type specifier가 되는 것을 막아요.

예를 들어

(defstruct (quux (:type list) :named) x y)

list가 만드는 것과 정확히 같은 list를 quux를 car로 하여 만드는 constructor를 만들어야 해요.

이 타입이 정의되면:

(deftype quux () '(satisfies quux-p))

이 form:

(typep (make-quux) 'quux)

은 이 form이 돌려주는 것과 정확히 같은 것을 돌려줘야 해요:

(typep (list 'quux nil nil) 'quux)

:type이 제공되지 않으면 구조체는 type structure-objectobject로 표현돼요.

:type 옵션 없는 defstruct는 구조체 이름을 이름으로 하는 class를 정의해요. 구조체 instancemetaclassstructure-class예요.

defstruct 구조체를 재정의하는 결과는 정의되지 않아요.

defstruct 옵션이 하나도 제공되지 않은 경우, 다음과 같은 함수들이 새 구조체의 인스턴스에 동작하도록 자동 정의돼요:

Predicatestructure-name-p라는 이름의 predicate가 구조체 타입 멤버십을 검사하도록 정의돼요. (structure-name-p object)는 object가 이 type이면 true, 아니면 false예요. typep도 새 type의 이름으로 objecttype에 속하는지 검사하는 데 쓸 수 있어요. (typep object '*structure-name*) 형태예요.

Component reader functions — 구조체 컴포넌트를 read하는 Reader 함수가 정의돼요. 각 슬롯 이름에 대해 structure-name-slot-name이라는 이름의 대응 reader 함수가 있어요. 이 함수는 그 슬롯의 내용을 read해요. 각 reader 함수는 구조체 타입 인스턴스 하나를 인자로 받아요. setf는 이 reader 함수 중 아무 것이나 슬롯 내용 변경에 쓸 수 있어요.

Constructor function — make-structure-name이라는 이름의 constructor 함수가 정의돼요. 이 함수는 구조체 타입의 새 인스턴스를 만들고 돌려줘요.

Copier function — copy-structure-name이라는 이름의 copier 함수가 정의돼요. copier 함수는 구조체 타입의 객체를 받아 첫 번째의 복사본인 같은 타입의 새 객체를 만들어요. copier 함수는 원본과 같은 컴포넌트 항목을 가진 새 구조체를 만들어요. 두 구조체 인스턴스의 대응 컴포넌트는 eql이에요.

defstruct formtop level form으로 나타나면, compilerstructure type 이름이 이후 선언에서 유효한 type 이름으로 인식되게 하고(deftype처럼), 구조체 슬롯 reader를 setf에 알려야 해요. 게다가 compilerstructure type에 대한 충분한 정보를 저장해서, 이후 defstruct 정의가 같은 file의 이후 deftype에서 :include를 써서 structure type 이름을 참조할 수 있게 해야 해요. defstruct가 생성한 함수는 컴파일 타임 환경에는 정의되지 않지만, compiler는 이후 호출을 inline으로 코딩할 수 있을 만큼 함수에 대한 정보를 저장할 수 있어요. #S reader macro는 새로 정의된 structure type 이름을 컴파일 시점에 인식할 수도 있고 아닐 수도 있어요.

예제 (Examples)

구조체 정의 예:

(defstruct ship
  x-position
  y-position
  x-velocity
  y-velocity
  mass)

이것은 모든 ship이 다섯 개의 이름 붙은 컴포넌트를 가진 object임을 선언해요. 이 form의 평가는 다음을 수행해요:

  • ship-x-position을 한 인자(ship)를 받아 ship의 x-position을 돌려주는 함수로 정의해요. ship-y-position과 다른 컴포넌트들도 비슷한 함수 정의를 받아요. 이 함수들은 구조체 요소를 access하는 데 쓰이므로 access 함수라고 불러요.
  • ship이 ship 인스턴스가 요소인 type의 이름이 돼요. ship은 typep에 받아들여져요. 예를 들어 (typep x 'ship)은 x가 ship이면 true이고 x가 ship 외의 object면 false예요.
  • 한 인자의 ship-p 함수가 정의돼요. 인자가 ship이면 true, 아니면 false인 predicate예요.
  • make-ship라는 함수가 정의되는데, 호출되면 access 함수와 쓰기 적합한 다섯 컴포넌트 데이터 구조체를 만들어요. 따라서
(setq ship2 (make-ship))

는 ship2를 새로 만들어진 ship object로 설정해요. make-ship 호출에서 키워드 인자로 원하는 컴포넌트의 초기값을 줄 수 있어요:

(setq ship2 (make-ship :mass *default-ship-mass*
                       :x-position 0
                       :y-position 0))

이것은 새 ship을 만들고 컴포넌트 세 개를 초기화해요. 이 함수는 새 구조체를 만들기 때문에 "constructor function"이라고 불러요.

  • 한 인자의 copy-ship 함수가 정의되는데, ship object가 주어지면 주어진 것의 복사본인 새 ship object를 만들어요. 이 함수는 "copier function"이라고 불러요.

setf로 ship의 컴포넌트를 변경할 수 있어요:

(setf (ship-x-position ship2) 100)

이것은 ship2의 x-position을 100으로 바꿔요. defstruct가 각 access 함수에 적절한 defsetf를 생성한 것처럼 동작하기 때문이에요.

;;;
;;; Example 1
;;; define town structure type
;;; area, watertowers, firetrucks, population, elevation are its components
;;;
(defstruct town
            area
            watertowers
            (firetrucks 1 :type fixnum)    ;an initialized slot
            population
            (elevation 5128 :read-only t)) ;a slot that can't be changed
=>  TOWN

;create a town instance
(setq town1 (make-town :area 0 :watertowers 0)) =>  #S(TOWN...)
;town's predicate recognizes the new instance
(town-p town1) =>  true
;new town's area is as specified by make-town
(town-area town1) =>  0
;new town's elevation has initial value
(town-elevation town1) =>  5128
;setf recognizes reader function
(setf (town-population town1) 99) =>  99
(town-population town1) =>  99
;copier function makes a copy of town1
(setq town2 (copy-town town1)) =>  #S(TOWN...)
(= (town-population town1) (town-population town2))  =>  true
;since elevation is a read-only slot, its value can be set only
;when the structure is created
(setq town3 (make-town :area 0 :watertowers 3 :elevation 1200))
=>  #S(TOWN...)
;;;
;;; Example 2
;;; define clown structure type
;;; this structure uses a nonstandard prefix
;;;
(defstruct (clown (:conc-name bozo-))
            (nose-color 'red)
            frizzy-hair-p polkadots) =>  CLOWN

(setq funny-clown (make-clown)) =>  #S(CLOWN)
;use non-default reader name
(bozo-nose-color funny-clown) =>  RED

(defstruct (klown (:constructor make-up-klown) ;similar def using other
            (:copier clone-klown)              ;customizing keywords
            (:predicate is-a-bozo-p))
            nose-color frizzy-hair-p polkadots) =>  klown
;custom constructor now exists
(fboundp 'make-up-klown) =>  true
;;;
;;; Example 3
;;; define a vehicle structure type
;;; then define a truck structure type that includes
;;; the vehicle structure
;;;
(defstruct vehicle name year (diesel t :read-only t)) =>  VEHICLE

(defstruct (truck (:include vehicle (year 79)))
            load-limit
            (axles 6)) =>  TRUCK

(setq x (make-truck :name 'mac :diesel t :load-limit 17))
=>  #S(TRUCK...)
;vehicle readers work on trucks
(vehicle-name x)
=>  MAC
;default taken from :include clause
(vehicle-year x)
=>  79

(defstruct (pickup (:include truck))     ;pickup type includes truck
            camper long-bed four-wheel-drive) =>  PICKUP

(setq x (make-pickup :name 'king :long-bed t)) =>  #S(PICKUP...)
;:include default inherited
(pickup-year x) =>  79
;;;
;;; Example 4
;;; use of BOA constructors
;;;
(defstruct (dfs-boa                      ;BOA constructors
              (:constructor make-dfs-boa (a b c))
              (:constructor create-dfs-boa
                (a &optional b (c 'cc) &rest d &aux e (f 'ff))))
            a b c d e f) =>  DFS-BOA

;a, b, and c set by position, and the rest are uninitialized
(setq x (make-dfs-boa 1 2 3)) =>  #(DFS-BOA...)
(dfs-boa-a x) =>  1

;a and b set, c and f defaulted
(setq x (create-dfs-boa 1 2)) =>  #(DFS-BOA...)
(dfs-boa-b x) =>  2
(eq (dfs-boa-c x) 'cc) =>  true

;a, b, and c set, and the rest are collected into d
(setq x (create-dfs-boa 1 2 3 4 5 6)) =>  #(DFS-BOA...)
(dfs-boa-d x) =>  (4 5 6)

예외 상황 (Exceptional Situations)

어떤 두 슬롯 이름(직접 있든 :include 옵션으로 상속됐든)이 string= 아래 같은 것이면 defstructtypeprogram-error인 에러를 신호해야 해요.

included-structure-namestructure type을 가리키지 않으면 결과는 정의되지 않아요.

더 알아보기 (See Also)

documentation, print-object, setf, subtypep, type-of, typep, Section 3.2 (Compilation)

Notes

printer-nameprint-escape 같은 printer 제어 변수의 값을 지켜봐야 해요.

slot-initform과 대응 슬롯의 :type 옵션 사이의 타입 불일치에 대한 경고를 내지 말아야 한다는 제한은, slot-option을 지정하려면 slot-initform을 지정해야 하고 어떤 경우에는 적절한 기본값이 없을 수 있기 때문에 필요해요.

defstruct가 슬롯 접근자를 setf와 함께 쓸 수 있게 만드는 메커니즘은 implementation-dependent해요. 예를 들어 setf function, setf expander, 또는 그 implementationsetf code가 아는 다른 implementation-dependent 메커니즘을 쓸 수 있어요.