객체와 클래스 계약

객체와 클래스 계약 (Object and Class Contracts)

클래스와 객체에 계약을 거는 방법을 살펴볼게요. class/c는 클래스의 외부·내부 계약을, object/c는 객체 단위의 계약을 다루고, 이 밖에 메서드 계약을 간단히 쓰게 해 주는 컴비네이터들이 준비되어 있어요.

출처: Racket Reference

본문

syntax

(class/c maybe-opaque member-spec ...)

 maybe-opaque =
               | #:opaque
               | #:opaque #:ignore-local-member-names
 member-spec =
             | method-spec
             | (field field-spec ...)
             | (init field-spec ...)
             | (init-field field-spec ...)
             | (inherit method-spec ...)
             | (inherit-field field-spec ...)
             | (super method-spec ...)
             | (inner method-spec ...)
             | (override method-spec ...)
             | (augment method-spec ...)
             | (augride method-spec ...)
             | (absent absent-spec ...)
 method-spec =
             | method-id
             | (method-id method-contract-expr)
 field-spec =
            | field-id
            | (field-id contract-expr)
 absent-spec =
             | method-id
             | (field field-id ...)

클래스에 대한 계약을 만들어 내요.

class/c 폼에 나열되는 계약은 외부 계약과 내부 계약, 두 큰 범주가 있어요. 외부 계약은 클래스에서 객체가 인스턴스화되거나, 그 클래스의 객체를 통해 메서드·필드에 접근할 때의 동작을 다뤄요. 내부 계약은 클래스 계층 안에서 메서드·필드에 접근할 때의 동작을 다뤄요. 이렇게 나누면 클래스 클라이언트에게는 더 강한 계약을, 서브클래스에게는 더 약한 계약을 줄 수 있어요.

메서드 계약은 메서드의 암시적 this 파라미터에 대응하는 초기 인자를 하나 더 포함해야 해요. 이렇게 하면 메서드가 호출될 때(또는 의존 계약의 경우 계약의 다른 부분에서) 객체의 상태를 다루는 계약을 쓸 수 있어요. ->m 같은 대안 계약 폼은 메서드 계약을 간단히 쓰기 위한 축약으로 제공돼요.

absent 절에 나열된 메서드·필드는 클래스에 없어야 해요.

클래스 계약은 #:opaque 키워드로 불투명하게 지정할 수 있어요. 불투명 클래스 계약은 계약이 지정한 외부 메서드·필드를 정확히 정의하는 클래스만 받아들여요. 계약된 클래스에 지정되지 않은 메서드·필드가 있으면 계약 오류가 발생해요. 로컬 멤버 이름(즉 define-local-member-name으로 정의된)의 메서드·필드는 #:ignore-local-member-names가 제공되면 이 검사에서 무시돼요.

외부 계약은 다음과 같아요:

  • 태그가 없는 외부 메서드 계약은 계약된 클래스의 객체에 대한 메서드 send에서 method-id의 구현 동작을 기술해요. 이 계약은 계약된 클래스의 구현이 동적 디스패치의 진입점이 될 수 없게 될 때까지 서브클래스에서도 계속 검사돼요.

필드 이름만 있으면, 메서드가 클래스에 존재한다는 것만 요구하는 것과 동등해요.

예시를 볼게요:

(define woody%
  (class object%
    (define/public (draw who)
      (format "reach for the sky, ~a" who))
    (super-new)))
(define/contract woody+c%
  (class/c [draw (->m symbol? string?)])
  woody%)

> (send (new woody%) draw #f)
"reach for the sky, #f"
> (send (new woody+c%) draw 'zurg)
"reach for the sky, zurg"
> (send (new woody+c%) draw #f)
draw: contract violation

  expected: symbol?

  given: #f

  in: the 1st argument of

      the draw method in

      (class/c (draw (->m symbol? string?)))

  contract from: (definition woody+c%)

  contract on: woody+c%

  blaming: top-level

   (assuming the contract is correct)

  at: eval:80:0
  • field로 태그된 외부 필드 계약은 클래스 밖에서 접근했을 때 그 필드에 담긴 값의 동작을 기술해요. 필드는 변이될 수 있으므로, 이 계약들은 필드의 외부 접근(get-field을 통한)과 외부 변이(set-field!을 통한) 모두에서 검사돼요.

필드 이름만 있으면, any/c 계약을 쓰는 것과 동등해요(하지만 더 효율적으로 검사돼요).

예시를 볼게요:

(define woody/hat%
  (class woody%
    (field [hat-location 'uninitialized])
    (define/public (lose-hat) (set! hat-location 'lost))
    (define/public (find-hat) (set! hat-location 'on-head))
    (super-new)))
(define/contract woody/hat+c%
  (class/c [draw (->m symbol? string?)]
           [lose-hat (->m void?)]
           [find-hat (->m void?)]
           (field [hat-location (or/c 'on-head 'lost)]))
  woody/hat%)

> (get-field hat-location (new woody/hat%))
'uninitialized
> (let ([woody (new woody/hat+c%)])
    (send woody lose-hat)
    (get-field hat-location woody))
'lost
> (get-field hat-location (new woody/hat+c%))
woody/hat+c%: broke its own contract

  promised: (or/c (quote on-head) (quote lost))

  produced: 'uninitialized

  in: the hat-location field in

      (class/c

       (draw (->m symbol? string?))

       (lose-hat (->m void?))

       (find-hat (->m void?))

       (field (hat-location

               (or/c 'on-head 'lost))))

  contract from: (definition woody/hat+c%)

  blaming: (definition woody/hat+c%)

   (assuming the contract is correct)

  at: eval:85:0
> (let ([woody (new woody/hat+c%)])
    (set-field! hat-location woody 'under-the-dresser))
woody/hat+c%: contract violation

  expected: (or/c (quote on-head) (quote lost))

  given: 'under-the-dresser

  in: the hat-location field in

      (class/c

       (draw (->m symbol? string?))

       (lose-hat (->m void?))

       (find-hat (->m void?))

       (field (hat-location

               (or/c 'on-head 'lost))))

  contract from: (definition woody/hat+c%)

  blaming: top-level

   (assuming the contract is correct)

  at: eval:85:0
  • init으로 태그된 초기화 인자 계약은 클래스 인스턴스화 중에 그 이름과 짝지어진 값의 기대 동작을 기술해요. 같은 이름이 두 번 이상 제공될 수 있는데, 그 경우 class/c 폼 안의 첫 번째 그런 계약이 초기화 인자 리스트에서 그 이름으로 태그된 첫 번째 값에 적용되고, 그다음도 마찬가지로 적용돼요.

초기화 인자 이름만 있으면 any/c 계약을 쓰는 것과 동등해요(하지만 더 효율적으로 검사돼요).

예시를 볼게요:

(define woody/init-hat%
  (class woody%
    (init init-hat-location)
    (field [hat-location init-hat-location])
    (define/public (lose-hat) (set! hat-location 'lost))
    (define/public (find-hat) (set! hat-location 'on-head))
    (super-new)))
(define/contract woody/init-hat+c%
  (class/c [draw (->m symbol? string?)]
           [lose-hat (->m void?)]
           [find-hat (->m void?)]
           (init [init-hat-location (or/c 'on-head 'lost)])
           (field [hat-location (or/c 'on-head 'lost)]))
  woody/init-hat%)

> (get-field hat-location
             (new woody/init-hat+c%
                  [init-hat-location 'lost]))
'lost
> (get-field hat-location
             (new woody/init-hat+c%
                  [init-hat-location 'slinkys-mouth]))
woody/init-hat+c%: contract violation

  expected: (or/c (quote on-head) (quote lost))

  given: 'slinkys-mouth

  in: the init-hat-location init argument in

      (class/c

       (draw (->m symbol? string?))

       (lose-hat (->m void?))

       (find-hat (->m void?))

       (init (init-hat-location

              (or/c 'on-head 'lost)))

       (field (hat-location

               (or/c 'on-head 'lost))))

  contract from: 

      (definition woody/init-hat+c%)

  blaming: top-level

   (assuming the contract is correct)

  at: eval:91:0
  • init-field 절에 나열된 계약은 각 계약이 init 절과 field 절에 나타난 것처럼 취급돼요.

내부 계약은 클래스와 그 서브클래스 사이에서 이루어지는 메서드 호출의 동작을 제한해요. 그런 호출은 위에서 설명한 클래스 계약에 의해 제어되지 않아요.

외부 계약과 마찬가지로, 메서드·필드 이름이 지정됐지만 계약이 나타나지 않으면, 대응하는 필드·메서드가 존재하기만 하면 계약이 충족돼요.

  • inherit로 태그된 메서드 계약은 계약된 클래스의 어떤 서브클래스에서 (즉 inherit를 통해) 직접 호출될 때 메서드의 동작을 기술해요. 이 계약은 외부 메서드 계약처럼, 계약된 클래스의 메서드 구현이 동적 디스패치의 진입점이 될 수 없게 될 때까지 적용돼요.

예시를 볼게요:

> (new (class woody+c%
         (inherit draw)
         (super-new)
         (printf "woody sez: “~a”\n" (draw "evil dr porkchop"))))
woody sez: “reach for the sky, evil dr porkchop”

(object:eval:94:0 ...)

> (define/contract woody+c-inherit%
    (class/c (inherit [draw (->m symbol? string?)]))
    woody+c%)
> (new (class woody+c-inherit%
         (inherit draw)
         (printf "woody sez: ~a\n" (draw "evil dr porkchop"))))
draw: contract violation

  expected: symbol?

  given: "evil dr porkchop"

  in: the 1st argument of

      the draw method in

      (class/c

       (inherit (draw (->m symbol? string?))))

  contract from: (definition woody+c-inherit%)

  contract on: woody+c-inherit%

  blaming: top-level

   (assuming the contract is correct)

  at: eval:95:0
  • super로 태그된 메서드 계약은 서브클래스의 super 폼이 호출할 때 method-id의 동작을 기술해요. 이 계약은 계약 클래스의 method-id 구현을 호출하는 서브클래스의 super 호출에만 영향을 줘요.

이 예시는 draw 메서드가 두 인자를 받으면 원래 draw 메서드에 대한 두 호출을 결합하도록 확장하는 방법을 보여줘요. 단, super 메서드가 어떻게 호출되어야 하는지를 제어하는 계약이 함께 붙어요.

예시를 볼게요:

(define/contract woody%+s
  (class/c (super [draw (->m symbol? string?)]))
  (class object%
    (define/public (draw who)
      (format "reach for the sky, ~a" who))
    (super-new)))
(define woody2+c%
  (class woody%+s
    (define/override draw
      (case-lambda
        [(a) (super draw a)]
        [(a b) (string-append (super draw a)
                              " and "
                              (super draw b))]))
    (super-new)))

> (send (new woody2+c%) draw 'evil-dr-porkchop  'zurg)
"reach for the sky, evil-dr-porkchop and reach for the sky, zurg"
> (send (new woody2+c%) draw "evil dr porkchop" "zurg")
draw: contract violation

  expected: symbol?

  given: "evil dr porkchop"

  in: the 1st argument of

      the draw method in

      (class/c

       (super (draw (->m symbol? string?))))

  contract from: (definition woody%+s)

  contract on: woody%+s

  blaming: top-level

   (assuming the contract is correct)

  at: eval:97:0

마지막 호출은 woody2%+c를 탓하며 오류를 신호해요. 초기 draw 호출을 검사하는 계약이 없고, super 호출이 자신의 계약을 위반하기 때문이에요.

  • inner로 태그된 메서드 계약은 클래스가 서브클래스의 augmenting 메서드에 기대하는 동작을 기술해요. 이 계약은 계약된 클래스에서 inner로 호출될 수 있는, 서브클래스의 method-id 구현들에 영향을 줘요. 즉 augmentoverment으로 method-id를 구현하는 서브클래스는 더 이상의 확장이 계약된 클래스를 통해 도달될 수 없으므로, 이후 서브클래스가 계약에 영향을 받지 않게 해요.

  • override로 태그된 메서드 계약은 직접 호출될 때(즉 (method-id ...) 적용으로) 계약된 클래스가 method-id에 기대하는 동작을 기술해요. 이 폼은 서브클래스에서 메서드를 오버라이드하는 것이 동적 디스패치 체인의 진입점을 바꿀 때에만(즉 메서드가 augmentable한 적이 없을 때) 사용할 수 있어요.

이번에는 두 인자를 지원하도록 draw를 오버라이드하는 대신, 두 인자를 받아 draw를 호출하는 새 메서드 draw2를 만들 수 있어요. 그리고 draw를 오버라이드해도 draw2가 깨지지 않게 하는 계약도 추가해요.

예시를 볼게요:

(define/contract woody2+override/c%
  (class/c (override [draw (->m symbol? string?)]))
  (class woody+c%
    (inherit draw)
    (define/public (draw2 a b)
      (string-append (draw a)
                     " and "
                     (draw b)))
    (super-new)))
(define woody2+broken-draw
  (class woody2+override/c%
    (define/override (draw x)
      'not-a-string)
    (super-new)))

> (send (new woody2+broken-draw) draw2
        'evil-dr-porkchop
        'zurg)
draw: contract violation

  expected: string?

  given: 'not-a-string

  in: the range of

      the draw method in

      (class/c

       (override (draw (->m symbol? string?))))

  contract from: 

      (definition woody2+override/c%)

  contract on: woody2+override/c%

  blaming: top-level

   (assuming the contract is correct)

  at: eval:101:0
  • augment 또는 augride로 태그된 메서드 계약은 서브클래스에서 직접 호출될 때 계약된 클래스가 method-id에 제공하는 동작을 기술해요. 이 폼은 메서드가 이전에 augmentable한 적이 있을 때에만 사용할 수 있어요. 이는 어떤 augmenting·overriding 구현도 동적 디스패치 체인의 진입점을 바꾸지 않는다는 뜻이에요. augment은 서브클래스가 메서드를 augment할 수 있을 때 쓰이고, augride는 서브클래스가 현재 augmentation을 override할 수 있을 때 쓰여요.

  • inherit-field로 태그된 필드 계약은 계약된 클래스의 어떤 서브클래스에서 (즉 inherit-field를 통해) 직접 접근할 때 그 필드에 담긴 값의 동작을 기술해요. 필드는 변이될 수 있으므로, 이 계약들은 그런 서브클래스에서 일어나는 필드의 접근 및/또는 변이 모두에서 검사돼요.

패키지 base의 버전 6.1.1.8에서 변경됨: 추가 키워드가 제공되면 불투명 class/c가 이제 로컬 멤버 이름을 선택적으로 무시해요.

syntax

(absent absent-spec ...)

class/c를 참고하세요. class/c 폼 밖에서 쓰면 구문 오류예요.

syntax

(->m dom ... range)

->와 비슷하지만, 결과 계약의 정의역이 명시된 정의역보다 요소 하나를 더 포함해요. 첫 (암시적) 인자는 any/c로 계약돼요. 이 계약은 this의 어떤 프로퍼티도 검사할 필요가 없을 때 더 간단한 메서드 계약을 쓰는 데 유용해요.

syntax

(->*m (mandatory-dom ...) (optional-dom ...) rest range)

->*와 비슷하지만, 결과 계약의 필수 정의역이 명시된 정의역보다 요소 하나를 더 포함해요. 첫 (암시적) 인자는 any/c로 계약돼요. 이 계약은 this의 어떤 프로퍼티도 검사할 필요가 없을 때 더 간단한 메서드 계약을 쓰는 데 유용해요.

syntax

(case->m (-> dom ... rest range) ...)

case->와 비슷하지만, 결과 계약의 각 case의 필수 정의역이 명시된 정의역보다 요소 하나를 더 포함해요. 첫 (암시적) 인자는 any/c로 계약돼요. 이 계약은 this의 어떤 프로퍼티도 검사할 필요가 없을 때 더 간단한 메서드 계약을 쓰는 데 유용해요.

syntax

(->dm (mandatory-dependent-dom ...)
      (optional-dependent-dom ...)
      dependent-rest
      pre-cond
      dep-range)

->d와 비슷하지만, 결과 계약의 필수 정의역이 명시된 정의역보다 요소 하나를 더 포함해요. 첫 (암시적) 인자는 any/c로 계약돼요. 게다가 this는 계약의 본문에서 적절히 바인딩돼요. 이 계약은 this의 어떤 프로퍼티도 검사할 필요가 없을 때 더 간단한 메서드 계약을 쓰는 데 유용해요.

syntax

(object/c member-spec ...)

 member-spec =
             | method-spec
             | (field field-spec ...)
             | #:opaque opaque-expr
             | #:opaque-except opaque-expr
             | #:opaque-fields opaque-fields-expr
             | #:do-not-check-class-field-accessor-or-mutator-access
 method-spec =
             | method-id
             | (method-id method-contract)
 field-spec =
            | field-id
            | (field-id contract-expr)

객체에 대한 계약을 만들어 내요. 각 필드와 메서드는 공급된 계약에 대해 검사돼요. 각 메서드 계약은 추가 "this" 인자를 받도록 작성되어야 한다는 점에 주의하세요. ->m이나 ->*m 계약 컴비네이터를 쓰는 것을 고려해 보세요.

opaque-expr이 있으면, 객체에 존재하지만 계약에 나열되지 않은 메서드를 어떻게 처리할지 제어해요:

  • opaque-expr#:opaque 키워드를 따르고 #f로 평가되면, 그런 메서드에 대한 호출은 항상 허용돼요.
  • #:opaque를 따르고 #t로 평가되면, 그런 메서드는 절대 허용되지 않아요.
  • #:opaque를 따르고 make-impersonator-property가 만들어 낸 술어 프로시저로 평가되면, 그 프로퍼티가 설정된 메서드 프로시저는 허용돼요.
  • opaque-expr#:opaque-except 키워드를 따르면 make-impersonator-property가 만들어 낸 술어 프로시저로 평가되어야 해요. 그 경우 그 프로퍼티가 있는 메서드는 허용되지 않고, 다른 메서드는 허용돼요.
  • opaque-expr이 없으면, 계약에 나열되지 않은 메서드에 대한 호출은 항상 허용돼요.

opaque-expr과 유사한 방식으로 필드에 대해, opaque-fields-expr이 객체에 존재하지만 계약에 나열되지 않은 필드를 어떻게 처리할지 제어해요:

  • opaque-fields-expr이 있고 #true로 평가되면, 계약에 나열되지 않은 필드는 접근이 허용되지 않아요.
  • opaque-fields-expr이 있고 #false로 평가되면, 그런 필드는 접근이 허용돼요.
  • opaque-fields-expr이 없고 opaque-exprmake-impersonator-property가 만들어 낸 술어 프로시저로 평가되면, 필드는 #:opaque가 있으면 허용되지 않고 #:opaque-except가 있으면 허용돼요.
  • opaque-fields-expr은 없지만 opaque-expr이 있으면, opaque-fields-expropaque-expr의 값에 기반해 기본값을 가져요. opaque-expr이 메서드를 허용하지 않으면 필드도 허용하지 않고, 허용하면 필드도 허용해요.
  • opaque-fields-expropaque-expr도 없으면, 나열되지 않은 모든 필드·메서드에 대한 접근이 허용돼요.

#:do-not-check-class-field-accessor-or-mutator-access가 있으면, class-field-mutatorclass-field-accessor가 돌려주는 프로시저를 통한 필드 접근은, (필드 접근이 허용되지 않거나 필드가 계약을 위반해서) 그렇지 않았다면 허용되지 않았을 상황에서도 항상 허용돼요. 이 동작은 문제가 있어 보이지만, 10년 넘게 object/c의 이전 버전에 존재했던 버그에 대응하는 것이므로, 이 옵션은 제대로 검사되는 계약으로의 전환 시점에 유연성을 주기 위해 여기 있어요.

procedure

(instanceof/c class-contract) → contract?
  class-contract : contract?

객체에 대한 계약을 만들어 내는데, 그 객체는 class-contract에 부합하는 클래스의 인스턴스예요.

procedure

(dynamic-object/c method-names
                  method-contracts
                  field-names
                  field-contracts) → contract?
  method-names : (listof symbol?)
  method-contracts : (listof contract?)
  field-names : (listof symbol?)
  field-contracts : (listof contract?)

object/c와 비슷하지만 메서드·필드의 이름과 계약을 동적으로 계산할 수 있는 객체 계약을 만들어 내요. 메서드와 필드 각각에 대해 이름 리스트와 계약 리스트의 길이는 같아야 해요.

syntax

(object-contract member-spec ...)

 member-spec =
             | (method-id method-contract)
             | (field field-id contract-expr)
 method-contract =
                 | (-> dom ... range)
                 |
                 | (->* (mandatory-dom ...)
                        (optional-dom ...)
                        rest
                        range)
                 |
                 | (->d (mandatory-dependent-dom ...)
                        (optional-dependent-dom ...)
                        dependent-rest
                        pre-cond
                        dep-range)
 dom = dom-expr
     | keyword dom-expr
 range = range-expr
       | (values range-expr ...)
       | any
 mandatory-dom = dom-expr
               | keyword dom-expr
 optional-dom = dom-expr
              | keyword dom-expr
 rest =
     |
     | #:rest rest-expr
 mandatory-dependent-dom = [id dom-expr]
                         | keyword [id dom-expr]
 optional-dependent-dom = [id dom-expr]
                        | keyword [id dom-expr]
 dependent-rest =
                |
                | #:rest id rest-expr
 pre-cond =
          |
          | #:pre-cond boolean-expr
 dep-range = any
           | [id range-expr] post-cond
           | (values [id range-expr] ...) post-cond
 post-cond =
           |
           | #:post-cond boolean-expr

객체에 대한 계약을 만들어 내요.

메서드의 각 계약은 대응하는 함수 계약과 같은 의미를 가지지만, 메서드 계약의 구문은 object-contract의 본문 안에 직접 작성되어야 해요. 클래스 정의의 메서드가 일반 함수 정의와 같은 구문을 쓰되 임의의 프로시저일 수는 없는 것과 비슷하지요. class/c의 메서드 계약과 달리, 암시적 this 인자는 계약의 일부가 아니에요. 의존 계약에서 this를 쓸 수 있도록, ->d 계약은 this를 객체 자체에 암시적으로 바인딩해요.

value

mixin-contract : contract?

믹스인(mixin)을 알아보는 함수 계약이에요. 함수의 입력이 클래스이고 결과가 입력의 서브클래스임을 보장해요.

procedure

(make-mixin-contract type ...) → contract?
  type : (or/c class? interface?)

함수의 입력이 각 type을 구현·서브클래싱하는 클래스이고, 결과가 입력의 서브클래스임을 보장하는 함수 계약을 만들어 내요.

procedure

(is-a?/c type) → flat-contract?
  type : (or/c class? interface?)

클래스나 인터페이스를 받아, 그 클래스/인터페이스를 인스턴스화한 객체를 알아보는 flat 계약을 돌려줘요.

is-a?를 참고하세요.

procedure

(implementation?/c interface) → flat-contract?
  interface : interface?

interface를 구현하는 클래스를 알아보는 flat 계약을 돌려줘요.

implementation?를 참고하세요.

procedure

(subclass?/c class) → flat-contract?
  class : class?

class의 서브클래스인 클래스를 알아보는 flat 계약을 돌려줘요.

subclass?를 참고하세요.

더 알아보기