값에 계약 붙이기
값에 계약 붙이기 (Attaching Contracts to Values)
모듈 경계에서만 계약을 붙일 수 있는 건 아니에요. contract-out·define/contract 같은 폼을 쓰면 프로시저 하나, 값 하나, 심지어 지역 경계에도 계약을 붙일 수 있죠. 이 문서는 값에 계약을 부착하는 각종 형태를 설명할게요.
출처: Racket Reference
본문
8.6 값에 계약 붙이기
contract-in과 contract-out
contract-in은 require 안에서, contract-out은 provide 안에서 써요(현재는 provide 폼과 같은 페이즈 레벨에서만; 예를 들어 contract-out은 for-syntax 안에 중첩될 수 없어요). contract-out의 각 식별자는 감싸는 모듈에서 제공되고, contract-in의 각 식별자는 지정된 모듈에서 요구돼요. 또한 각 내보내기마다 식별자 사용이 contract-expr이 지정하는 계약을 지켜야 해요.
(contract-in module-path in-out-item ...)
(contract-out unprotected-submodule in-out-item ...)
in-out-item = [id contract-expr]
| (rename internal-id external-id contract-expr)
| (struct id/ignored ([id contract-expr] ...)
struct-option)
| #:∃ poly-variables
| #:exists poly-variables
| #:∀ poly-variables
| #:forall poly-variables
unprotected-submodule = #:unprotected-submodule submodule-name
poly-variables = id | (id ...)
id/ignored = id | (id ignored-id)
struct-option = #:omit-constructor
contract-out과 contract-in 폼은 모듈을 blame의 단위로 취급해요. 각 식별자를 제공하는 모듈은 계약의 양성(공변, co-variant) 위치를 지켜야 하고, 제공된 변수를 가져오는 각 모듈은 계약의 음성(반변, contra-variant) 위치를 지켜야 해요. 계약이 붙은 변수의 사용은 그것을 제공한 모듈 밖에서만 검사돼요. 제공하는 모듈 안에서는 계약 검사가 일어나지 않죠.
contract-out 폼 안에서 각 contract-expr은 사실상 감싸는 모듈의 끝으로 옮겨져서, 같은 모듈에서 나중에 정의된 변수를 참조할 수 있어요.
rename폼은 첫 번째 변수(내부 이름)를 두 번째 변수가 지정하는 이름(외부 이름)으로 내보내요.struct폼은 구조체 타입 정의id에 계약을 부여하고, 각 필드에는 필드 내용을 규정하는 계약이 붙어요. 다만struct정의와 달리 모든 필드(와 그 계약)를 나열해야 해요. 하위 구조체가 부모와 공유하는 필드에 대한 계약은 하위 구조체의 생성자 계약에만 쓰이고, 상위 구조체의 선택자·뮤테이터는 제공되지 않아요. 내보내는 구조체 타입 이름은 원래 구조체 타입 이름이 생성자처럼 동작하지 않더라도 항상 생성자 역할을 겸해요.#:omit-constructor옵션이 있으면 생성자는 제공되지 않아요.id와ignored-id를 모두 갖는 두 번째id/ignored형태는 폐기(deprecated)되어 하위 호환용으로만 문법에 남아 있고,ignored-id는 무시돼요. 첫 번째 형태를 쓰는 게 좋아요.- 구조체가
serializable-struct나define-serializable-struct로 만들어졌다면,contract-out은deserialize로 생성된 구조체 인스턴스를 보호하지 않아요.struct-guard/c를 쓰는 걸 고려하세요. #:∃,#:exists,#:∀,#:forall절은 새로운 추상 계약을 정의해요. 변수들은contract-out폼의 나머지 부분에서 이들이 받아들이는 값을 숨기고 내보낸 함수가 파라메트릭하게 취급되도록 하는 새 계약에 바인딩돼요. 값 숨김 방식에 대한 상세는new-∃/c와new-∀/c를 참고하세요.#:unprotected-submodule이 나타나면, 그 뒤의 식별자는contract-out이 생성하는 하위 모듈의 이름으로 쓰여요. 그 하위 모듈은contract-out의 모든 이름을 계약 없이 내보내요. 특히 각struct폼에 대해 원래 구조체 타입 이름이 내보내지므로,#:omit-constructor는 (있을 경우) 추가 생성자만 생략해요.
contract-out 구현은 syntax-property를 써서 생성하는 코드에 완전히 확장된 프로그램의 계약 문법을 기록하는 속성을 붙여요. 구체적으로 'provide/contract-original-contract 기호에 두 원소짜리 벡터, 즉 내보내진 식별자와 그 내보내기를 제어하는 계약을 만드는 표현식에 대한 syntax 객체가 바인딩돼요.
예시:
> (module math-example racket/base
(require racket/contract)
; Compute the reciprocal of a real number
(define (recip x) (/ 1 x))
(provide
(contract-out
[recip (-> (and/c real? (not/c zero?)) real?)])))
> (require 'math-example)
> (recip 3)
1/3
> (recip 1+2i)
recip: contract violation
expected: real?
given: 1+2i
in: an and/c case of
the 1st argument of
(-> (and/c real? (not/c zero?)) real?)
contract from: math-example
blaming: top-level
(assuming the contract is correct)
at: eval:2:0
Changed in version 7.3.0.3 of package base: Added #:unprotected-submodule. Changed in version 7.7.0.9: Started ignoring ignored-id. Changed in version 8.12.0.13: Added contract-in Changed in version 8.13.0.1: Added rename and struct to contract-in
recontract-out
(recontract-out id ...)
provide에 쓰는 provide-spec이에요(현재는 contract-out처럼 provide 폼과 같은 페이즈 레벨에서만). id를 다시 내보내되, 양성 blame을 id 원래 위치 대신 recontract-out을 포함한 모듈에 돌려요. 공개 모듈이 비공개 모듈의 식별자를 내보내면서도, 계약 위반을 비공개 모듈 대신 공개 모듈 기준으로 보고하고 싶을 때 유용해요.
예시:
> (module private-implementation racket/base
(require racket/contract)
(define (recip x) (/ 1 x))
(define (non-zero? x) (not (= x 0)))
(provide/contract [recip (-> (and/c real? non-zero?)
(between/c -1 1))]))
> (module public racket/base
(require racket/contract
'private-implementation)
(provide (recontract-out recip)))
> (require 'public)
> (recip +nan.0)
recip: broke its own contract
promised: (between/c -1 1)
produced: +nan.0
in: the range of
(->
(and/c real? non-zero?)
(between/c -1 1))
contract from: public
blaming: public
(assuming the contract is correct)
at: eval:3:0
recontract-out 대신 그냥 recip를 쓰면 계약 위반이 비공개 모듈을 blame하게 돼요.
provide/contract
(provide/contract unprotected-submodule in-out-item ...)
(provide (contract-out unprotected-submodule in-out-item ...))의 레거시 축약형이에요. 단, provide/contract 안의 contract-expr은 감싸는 모듈 끝이 아니라 provide/contract 폼의 위치에서 평가돼요.
struct-guard/c
(struct-guard/c contract-expr ...)
struct, serializable-struct(및 관련 폼)에 #:guard 인자로 넘길 프로시저를 반환해요. 이 가드 프로시저는 구조체가 뮤테이트되지 않는 한 각 계약이 해당 필드 값을 보호하도록 보장해요. 뮤테이션은 보호되지 않아요.
예시:
> (struct snake (weight hungry?)
#:guard (struct-guard/c real? boolean?))
> (snake 1.5 "yep")
snake, field 2: contract violation
expected: boolean?
given: "yep"
in: boolean?
contract from: top-level
blaming: top-level
(assuming the contract is correct)
at: eval:2:0
8.6.1 중첩 계약 경계 (Nested Contract Boundaries)
racket/contract/region 라이브러리(package: base)를 요구하면 다음 폼들을 쓸 수 있어요.
with-contract
(with-contract blame-id (wc-export ...) free-var-list ... body ...+)
(with-contract blame-id results-spec free-var-list ... body ...+)
wc-export = (id contract-expr)
result-spec = #:result contract-expr
| #:results (contract-expr ...)
free-var-list = #:freevar id contract-expr
| #:freevars ([id contract-expr] ...)
지역 계약 경계를 만들어요.
첫 번째 with-contract 폼은 표현식 위치에 올 수 없어요. 이 폼 안에서 정의된 모든 이름은 외부에서 보이지만, wc-export 목록에 있는 이름들은 해당 계약으로 보호돼요. 폼의 본문은(그 문맥이 허용한다면) 정의와 표현식이 섞일 수 있어요.
두 번째 with-contract 폼은 표현식 위치에 와야 해요. 마지막 본문 표현식은 result-spec에 나열된 계약 수만큼의 값을 반환해야 하고, 반환된 각 값은 각자의 계약으로 계약돼요. 본문 폼들의 순서는 let처럼 취급돼요.
blame-id는 내보낸 id와 짝을 이루는 계약의 양성 위치에 쓰여요. with-contract 본문 안에서 깨진 계약은 음성 위치로 blame-id를 써요.
free-var-list가 주어지면, 본문 안에서 자유 변수가 사용될 때 그것을 보호하는 계약이 붙어, 양성 위치는 with-contract 폼의 문맥을, 음성 위치는 with-contract 폼 자체를 blame해요.
define/contract
(define/contract id contract-expr free-var-list init-value-expr)
(define/contract (head args) contract-expr free-var-list body ...+)
define처럼 동작하되, 계약 contract-expr이 바인딩된 값에 붙어요. head와 args의 정의는 define을 참고하고, free-var-list의 정의는 with-contract를 참고하세요.
define/contract 폼은 개별 정의를 하나의 계약 영역으로 취급해요. 정의 자체가 계약의 양성(공변) 위치를 책임지고, 정의 밖에서의 id 참조는 계약의 음성 위치를 지켜야 해요. 계약 경계가 정의와 주변 문맥 사이에 있으므로 define/contract 폼 안에서의 id 참조는 검사되지 않아요.
예시:
; an unsual predicate that prints when called
> (define (printing-int? x)
(displayln "I was called")
(exact-integer? x))
> (define/contract (fact n)
(-> printing-int? printing-int?)
(if (zero? n)
1
(* n (fact (sub1 n)))))
> (fact 5) ; only prints twice, not for each recursive call
I was called
I was called
120
free-var-list가 주어지면, 본문 안에서 자유 변수가 사용될 때 그것을 보호하는 계약이 붙어, 양성 위치는 define/contract 폼의 문맥을, 음성 위치는 define/contract 폼 자체를 blame해요.
예시:
> (define (integer->binary-string n)
(number->string n 2))
> (define/contract (numbers->strings lst)
(-> (listof number?) (listof string?))
#:freevar integer->binary-string (-> exact-integer? string?)
; mistake, lst might contain inexact numbers
(map integer->binary-string lst))
> (numbers->strings '(4.0 3.3 5.8))
integer->binary-string: contract violation
expected: exact-integer?
given: 4.0
in: the 1st argument of
(-> exact-integer? string?)
contract from: top-level
blaming: (function numbers->strings)
(assuming the contract is correct)
at: eval:3:0
struct/contract
(struct/contract struct-id ([field contract-expr] ...)
struct-option ...)
(struct/contract struct-id super-struct-id
([field contract-expr] ...)
struct-option ...)
struct처럼 동작하되, 생성자·접근자·뮤테이터의 인자가 계약으로 보호돼요. field와 struct-option의 정의는 struct를 참고하세요. struct/contract 폼은 struct-option 키워드의 부분집합만 허용해요: #:mutable, #:transparent, #:auto-value, #:omit-define-syntaxes, #:property.
예시:
> (struct/contract fruit ([seeds number?]))
> (fruit 60)
#<fruit>
> (fruit #f)
fruit: contract violation
expected: number?
given: #f
in: the 1st argument of
(-> number? symbol? any)
contract from: (struct fruit)
blaming: top-level
(assuming the contract is correct)
> (struct/contract apple fruit ([type string?]))
> (apple 14 "golden delicious")
#<apple>
> (apple 5 30)
apple: contract violation
expected: string?
given: 30
in: the 2nd argument of
(-> any/c string? symbol? any)
contract from: (struct apple)
blaming: top-level
(assuming the contract is correct)
> (apple #f "granny smith")
fruit: contract violation
expected: number?
given: #f
in: the 1st argument of
(-> number? symbol? any)
contract from: (struct fruit)
blaming: top-level
(assuming the contract is correct)
define-struct/contract
(define-struct/contract struct-id ([field contract-expr] ...)
struct-option ...)
(define-struct/contract (struct-id super-struct-id)
([field contract-expr] ...)
struct-option ...)
struct/contract처럼 동작하되, super-struct-id를 주는 문법이 다르고, struct-id에 make- 접두가 붙은 생성자 id가 암묵적으로 제공돼요. field와 struct-option의 정의는 define-struct를 참고하세요. struct 대 define-struct의 관계처럼, 보통 struct/contract가 define-struct/contract보다 선호돼요. define-struct/contract 폼은 struct-option 키워드의 부분집합만 허용해요: #:mutable, #:transparent, #:auto-value, #:omit-define-syntaxes, #:property.
예시:
> (define-struct/contract fish ([color number?]))
> (make-fish 5)
#<fish>
> (make-fish #f)
make-fish: contract violation
expected: number?
given: #f
in: the 1st argument of
(-> number? symbol? any)
contract from: (struct fish)
blaming: top-level
(assuming the contract is correct)
> (define-struct/contract (salmon fish) ([ocean symbol?]))
> (make-salmon 5 'atlantic)
#<salmon>
> (make-salmon 5 #f)
make-salmon: contract violation
expected: symbol?
given: #f
in: the 2nd argument of
(-> any/c symbol? symbol? any)
contract from: (struct salmon)
blaming: top-level
(assuming the contract is correct)
> (make-salmon #f 'pacific)
make-fish: contract violation
expected: number?
given: #f
in: the 1st argument of
(-> number? symbol? any)
contract from: (struct fish)
blaming: top-level
(assuming the contract is correct)
invariant-assertion
(invariant-assertion invariant-expr expr)
invariant-expr로 결정되는 expr의 불변식(invariant)을 확립해요. 계약 명세와 달리 invariant-assertion은 두 당사자 사이의 경계를 만들지 않아요. 대신 값에 논리적 단언(assertion)을 붙일 뿐이죠. 이 폼은 단언을 검사할 때 계약 매커니즘을 쓰기 때문에, 단언 위반에 대한 blame 대상은 감싸는 모듈로 취급돼요.
즉, 단언은 재귀 호출에서도 검사돼요. 예를 들어 정의의 우변에서 불변식을 쓸 때:
> (define furlongss->feets
(invariant-assertion
(-> (listof real?) (listof real?))
(λ (l)
(cond
[(empty? l) empty]
[else
(if (= 327 (car l))
(furlongss->feets (list "wha?"))
(cons (furlongs->feet (first l))
(furlongss->feets (rest l))))]))))
> (furlongss->feets (list 1 2 3))
'(660 1320 1980)
> (furlongss->feets (list 1 327 3))
furlongss->feets: assertion violation
expected: real?
given: "wha?"
in: an element of
the 1st argument of
(-> (listof real?) (listof real?))
contract from: invariant-assertion
at: eval:5:0
Added in version 6.0.1.11 of package base.
current-contract-region
current-contract-region
define-syntax-parameter로 바인딩되는 매개변수로, 현재 계약 영역에 대한 정보를 담아요. 위 폼들이 blame 할당의 후보를 결정할 때 이 정보를 써요.
8.6.2 저수준 계약 경계 (Low-level Contract Boundaries)
define-module-boundary-contract
(define-module-boundary-contract id
orig-id
contract-expr
d-m-b-c-kwd-arg ...)
d-m-b-c-kwd-arg = #:name-for-contract name-for-contract-id
| #:name-for-blame blame-id
| #:srcloc srcloc-expr
| #:pos-source pos-source-expr
| #:context-limit limit-expr
| #:lift-to-end? boolean
| #:start-swapped? boolean
id를 orig-id로 정의하되 contract-expr 계약을 붙여요.
id는 매크로 변환자로 정의되는데, 사용 문맥을 참고해 음성 blame 할당 이름을 결정해요(참조가 나타나는 전체 모듈을 음성 당사자로 써요). 오류 메시지에 쓰일 이름은 #:name-for-blame이 제공되지 않으면 orig-id이고, 제공되면 그 뒤의 식별자가 오류 메시지의 이름으로 쓰여요.
계약 표현식은 let으로 감싸져 특정 상황(예: 계약이 함수 계약일 때)에서 감싼 값의 이름으로 넘겨질 이름을 가져요. name-for-contract-id가 제공되면 그 뒤의 식별자가 계약 이름으로 쓰이고, 그렇지 않으면 orig-id가 쓰여요.
값에 계약을 붙인 위치에 대한 blame 오류 메시지의 소스 위치는 기본적으로 define-module-boundary-contract 사용 위치의 소스 위치이지만, #:srcloc 인자로 지정할 수 있어요. 그 경우 datum->syntax의 세 번째 인자로 받을 수 있는 것 중 아무거나 될 수 있어요.
양성 당사자는 기본적으로 define-module-boundary-contract 사용을 포함한 모듈이지만, #:pos-source 키워드로 명시할 수 있어요.
#:context-limit이 주어지면 contract에 줄 때와 동일하게 동작해요.
lift-to-end?가 #t이거나 주어지지 않으면, 계약 표현식은 감싸는 모듈의 끝에 배치돼요(syntax-local-lift-module-end-declaration 사용). 주어지고 #f이면 계약 표현식은 define-module-boundary-contract가 놓인 위치에 배치돼요.
start-swapped?가 #t이면 초기 blame 객체가 "swapped" 상태로 만들어지고 pos-source가 음성 소스로 쓰여요. 특정 상황에서 계약 위반의 "contract from:" 줄을 정확히 얻는 데 도움이 돼요. #:start-swapped?가 주어지지 않으면 #f로 주어진 것처럼 취급돼요.
예시:
> (module server racket/base
(require racket/contract/base)
(define (f x) #f)
(define-module-boundary-contract g f (-> integer? integer?))
(provide g))
> (module client racket/base
(require 'server)
(define (clients-fault) (g #f))
(define (servers-fault) (g 1))
(provide servers-fault clients-fault))
> (require 'client)
> (clients-fault)
g: contract violation
expected: integer?
given: #f
in: the 1st argument of
(-> integer? integer?)
contract from: 'server
blaming: client
(assuming the contract is correct)
at: eval:2:0
> (servers-fault)
g: broke its own contract
promised: integer?
produced: #f
in: the range of
(-> integer? integer?)
contract from: 'server
blaming: (quote server)
(assuming the contract is correct)
at: eval:2:0
Changed in version 6.7.0.4 of package base: Added the #:name-for-blame argument. Changed in version 6.90.0.29: Added the #:context-limit argument. Changed in version 8.13.0.1: Added the #:name-for-contract and #:start-swapped arguments.
contract
(contract contract-expr to-protect-expr
positive-blame-expr negative-blame-expr)
(contract contract-expr to-protect-expr
positive-blame-expr negative-blame-expr
#:context-limit limit-expr)
(contract contract-expr to-protect-expr
positive-blame-expr negative-blame-expr
value-name-expr source-location-expr)
값에 계약을 붙이는 저수준 메커니즘이에요. contract의 목적은 어떤 고수준 계약 지정 폼이 확장될 때의 대상이 되는 거예요.
contract 표현식은 contract-expr이 지정한 계약을 to-protect-expr이 만든 값에 더해요. contract 표현식의 결과는 to-protect-expr 표현식의 결과이되, contract-expr이 지정한 계약이 to-protect-expr에 적용돼요.
positive-blame-expr과 negative-blame-expr의 값은 contract-expr이 지정한 계약의 양성·음성 위치에 대한 blame을 어떻게 할당할지 나타내요. 두 값은 아무 값이어도 되고, 계약 위반 오류 메시지 목적으로 display가 표시하는 방식으로 포매팅돼요.
value-name-expr이 지정되면 오류 메시지에 쓰일 보호된 값의 이름을 나타내요. 주어지지 않거나 value-name-expr이 #f를 만들면 이름은 출력되지 않아요. 그 외에는 역시 display가 표시하는 방식으로 포매팅돼요. 정확히는 value-name-expr이 blame 레코드의 blame-value 필드에 들어가고, 그 값이 오류 메시지의 첫 부분으로 쓰여요.
예시:
> (contract integer? #f 'pos 'neg 'timothy #f)
timothy: broke its own contract
promised: integer?
produced: #f
in: integer?
contract from: pos
blaming: pos
(assuming the contract is correct)
> (contract integer? #f 'pos 'neg #f #f)
broke its own contract
promised: integer?
produced: #f
in: integer?
contract from: pos
blaming: pos
(assuming the contract is correct)
source-location-expr이 지정되면 계약 위반이 보고하는 소스 위치를 나타내요. 이 표현식은 srcloc 구조체, syntax 객체, #f, 또는 datum->syntax의 세 번째 인자가 받는 형식의 리스트·벡터 중 하나를 만들어야 해요.
#:context-limit이 주어지면 그 뒤의 표현식이 #f 또는 자연수로 평가돼야 해요. 자연수로 평가되면 문맥 정보의 계층 수가 최대 그만큼으로 제한돼요. 예를 들어 그 수가 0이면 문맥 정보가 기록되지 않고 오류 메시지에 in:으로 시작하는 부분이 들어가지 않아요.