함수 컨트랙트
함수 컨트랙트 (Function Contracts)
프로시저를 감싸 인자와 결과에 대한 검사를 지연시키는 함수 컨트랙트를 알아봐요. ->, ->*, ->i 등 다양한 컨트랙트 조합자와 그 표현력을 다룹니다.
출처: Racket Reference
본문
8.2 함수 컨트랙트
함수 컨트랙트는 프로시저를 감싸 인자와 결과에 대한 검사를 지연시켜요. 세 개의 주요 함수 컨트랙트 조합자가 있는데, 표현력이 점점 증가하고 추가 오버헤드도 점점 늘어납니다. 첫 번째 ->가 가장 저렴해요. 원래 함수를 직접 호출할 수 있는 래퍼 함수를 생성합니다. ->*로 만든 컨트랙트는 래퍼 함수에서 인자를 목록으로 묶은 다음 keyword-apply나 apply 중 하나를 사용해야 해요. 마지막으로 ->i는 (->d와 함께) 가장 비싼데, 도메인과 레인지의 컨트랙트 표현식 평가를 함수 자체가 호출되거나 반환될 때까지 지연시키기 때문이에요.
case-> 컨트랙트는 case-lambda와 일치하도록 설계된 특수 컨트랙트이고, unconstrained-domain->는 도메인이 특정 형태일 것을 요구하지 않고 레인지 검사를 허용합니다 (아래 예시 사용 참고).
(-> dom ... range) (-> dom ... ellipsis dom-expr ... range)dom | = | dom-expr | | keyword dom-expr range | = | range-expr | | (values range-expr ...) | | any ellipsis | = | ...
dom-expr 컨트랙트가 지정한 인자를 받고, 고정된 수의 결과 또는 완전히 미지정된 결과(후자는 any가 지정될 때)를 반환하는 함수에 대한 컨트랙트를 만듭니다.
각 dom-expr은 함수 인자에 대한 컨트랙트이고, 각 range-expr은 함수 결과에 대한 컨트랙트예요.
도메인이 ...을 포함하면, 함수는 도메인 부분의 나머지 컨트랙트가 지정한 만큼의 인자를 받을 뿐 아니라, ... 바로 앞의 컨트랙트와 일치하는 임의로 더 많은 인자도 받습니다. 그렇지 않으면 컨트랙트는 정확히 지정된 인자만 받아요.
(공백으로 구분된 두
.사이에->를 쓰는 것은 여는 괄호 바로 뒤에->를 놓는 것과 같습니다. 자세한 내용은 Lists and Racket Syntax 또는 Reading Pairs and Lists를 참고하세요.)
예를 들어
(integer? boolean? . -> . integer?)
는 두 인자를 가진 함수에 대한 컨트랙트를 만들어요. 첫 번째 인자는 정수여야 하고, 두 번째 인자는 불리언이어야 합니다. 함수는 정수를 만들어야 해요.
예시:
(define/contract (maybe-invert i b)
(-> integer? boolean? integer?)
(if b (- i) i))
(maybe-invert 1 #t)
; -1
(maybe-invert #f 1)
; maybe-invert: contract violation
; expected: integer?
; given: #f
; in: the 1st argument of
; (-> integer? boolean? integer?)
; contract from: (function maybe-invert)
; blaming: top-level
; (assuming the contract is correct)
; at: eval:2:0
도메인 명세는 키워드를 포함할 수 있어요. 그렇다면 함수는 대응하는 (필수) 키워드 인자를 받아야 하고, 키워드 인자의 값은 대응하는 컨트랙트와 일치해야 해요. 예를 들어:
(integer? #:invert? boolean? . -> . integer?)
는 위치 인자로 정수를 받고 #:invert? 인자로 불리언을 받는 함수에 대한 컨트랙트예요.
예시:
(define/contract (maybe-invert i #:invert? b)
(-> integer? #:invert? boolean? integer?)
(if b (- i) i))
(maybe-invert 1 #:invert? #t)
; -1
(maybe-invert 1 #f)
; maybe-invert: arity mismatch;
; the expected number of arguments does not match the given
; number
; expected: 1 plus an argument with keyword #:invert?
; given: 2
; arguments...:
; 1
; #f
...을 사용하는 예로, 이 컨트랙트:
(integer? string? ... integer? . -> . any)
는 함수의 첫 번째와 마지막 인자가 정수여야 하고(그리고 최소 두 개의 인자가 있어야 하고) 다른 모든 인자가 문자열이어야 함을 강조합니다.
예시:
(define/contract (string-length/between? lower-bound s1 . more-args)
(-> integer? string? ... integer? boolean?)
(define all-but-first-arg-backwards (reverse (cons s1 more-args)))
(define upper-bound (first all-but-first-arg-backwards))
(define strings (rest all-but-first-arg-backwards))
(define strings-length
(for/sum ([str (in-list strings)])
(string-length str)))
(<= lower-bound strings-length upper-bound))
(string-length/between? 4 "farmer" "john" 40)
; #t
(string-length/between? 4 "farmer" 'john 40)
; string-length/between?: contract violation
; expected: string?
; given: 'john
; in: the repeated argument of
; (-> integer? string? ... integer? boolean?)
; contract from:
; (function string-length/between?)
; blaming: top-level
; (assuming the contract is correct)
; at: eval:2:0
(string-length/between? 4 "farmer" "john" "fourty")
; string-length/between?: contract violation
; expected: integer?
; given: "fourty"
; in: the last argument of
; (-> integer? string? ... integer? boolean?)
; contract from:
; (function string-length/between?)
; blaming: top-level
; (assuming the contract is correct)
; at: eval:2:0
->의 마지막 하위 폼으로 any를 사용하면, 함수의 결과에 대한 컨트랙트 검사가 수행되지 않아 어떤 개수의 값도 합법입니다(함수의 호출마다 개수가 달라도 됨).
예시:
(define/contract (multiple-xs n x)
(-> natural? any/c any)
(apply
values
(for/list ([_ (in-range n)])
n)))
(multiple-xs 4 "four")
; 4
; 4
; 4
; 4
->의 마지막 하위 폼으로 (values range-expr ...)을 사용하면, 함수는 각 컨트랙트에 대해 하나의 결과를 만들어야 하고 각 값은 각자의 컨트랙트와 일치해야 해요.
예시:
(define/contract (multiple-xs n x)
(-> natural? any/c (values any/c any/c any/c))
(apply
values
(for/list ([_ (in-range n)])
n)))
(multiple-xs 3 "three")
; 3
; 3
; 3
(multiple-xs 4 "four")
; multiple-xs: broke its own contract;
; expected 3 values, returned 4 values
; in: the range of
; (->
; natural?
; any/c
; (values any/c any/c any/c))
; contract from: (function multiple-xs)
; blaming: (function multiple-xs)
; (assuming the contract is correct)
; at: eval:2:0
버전 6.4.0.5의 package base에서 변경됨: 줄임표 지원 추가.
(->* (mandatory-dom ...) optional-doms rest pre range post)mandatory-dom | = | dom-expr | | keyword dom-expr optional-doms | = | (optional-dom ...) optional-dom | = | dom-expr | | keyword dom-expr rest | = | #:rest rest-expr pre | = | #:pre pre-cond-expr | | #:pre/desc pre-cond-expr range | = | range-expr | | (values range-expr ...) | | any post | = | #:post post-cond-expr | | #:post/desc post-cond-expr
->* 컨트랙트 조합자는 선택 인자(키워드 또는 위치) 또는 임의로 많은 인자를 받는 함수에 대한 컨트랙트를 만듭니다. ->* 컨트랙트의 첫 번째 절은 필수 인자를 설명하고, -> 컨트랙트의 인자 설명과 유사해요. 두 번째 절은 선택 인자를 설명합니다. 레인지 설명은 any이거나 컨트랙트 시퀀스일 수 있는데, 후자는 함수가 여러 값을 반환해야 함을 나타냅니다.
존재한다면 rest-expr 컨트랙트는 rest 매개변수의 인자를 지배합니다. rest-expr 컨트랙트가 rest 매개변수의 인자만 지배하지 필수 인자는 지배하지 않는다는 점을 유의하세요. 예를 들어 이 컨트랙트:
(->* () #:rest (cons/c integer? (listof integer?)) any)
는 함수
(λ (x . rest) x)
와 일치하지 않아요. 컨트랙트가 함수가 0개의 인자를 받는다고 강조하기 때문입니다(컨트랙트에 필수 인자가 없으므로). ->* 컨트랙트는 rest 인자의 컨트랙트가 결국 빈 인자 목록을 불허하게 될 것임을 알지 못해요.
pre-cond-expr과 post-cond-expr 표현식은 각각 함수가 호출될 때와 반환될 때 검사되며, 인자(또는 결과)와의 명시적 연결 없이 환경을 검사할 수 있게 해줘요. #:pre나 #:post 키워드를 사용하면 #f 결과가 실패로 취급되고 다른 결과는 성공으로 취급됩니다. #:pre/desc나 #:post/desc 키워드를 사용하면 표현식의 결과는 불리언, 문자열, 또는 문자열 목록 중 하나여야 하고, #t는 성공을 뜻하며 다른 결과는 실패를 뜻해요. 결과가 문자열이나 문자열 목록이면, 문자열은 각 줄바꿈 뒤에 정확히 한 칸의 공백을 가질 것으로 기대되며 여러 개는 오류 메시지의 줄로 사용됩니다. 그 경우 컨트랙트 자체가 각 문자열에 한 칸의 들여쓰기를 추가해요. 형식 요구 사항은 검사되지 않지만 Error Message Conventions의 권장 사항과 일치합니다.
예로, 컨트랙트
(->* () (boolean? #:x integer?) #:rest (listof symbol?) symbol?)
는 선택적으로 불리언, 정수 키워드 인자 #:x, 그리고 임의로 더 많은 심볼을 받고 심볼을 반환하는 함수와 일치합니다.
(->i maybe-chaperone (mandatory-dependent-dom ...) dependent-rest pre-condition param-value dependent-range post-condition)(->i maybe-chaperone (mandatory-dependent-dom ...) (optional-dependent-dom ...) dependent-rest pre-condition param-value dependent-range post-condition)maybe-chaperone | = | #:chaperone mandatory-dependent-dom | = | id+ctc | | keyword id+ctc optional-dependent-dom | = | id+ctc | | keyword id+ctc dependent-rest | = | #:rest id+ctc pre-condition | = | #:pre (id ...) boolean-expr pre-condition | | #:pre/desc (id ...) expr pre-condition | | #:pre/name (id ...) string boolean-expr pre-condition param-value | = | #:param (id ...) param-expr val-expr param-value dependent-range | = | any | | id+ctc | | un+ctc | | (values id+ctc ...) | | (values un+ctc ...) post-condition | = | #:post (id ...) boolean-expr post-condition | | #:post/desc (id ...) expr post-condition | | #:post/name (id ...) string boolean-expr post-condition id+ctc | = | [id contract-expr] | | [id (id ...) contract-expr] un+ctc | = | [_ contract-expr] | | [_ (id ...) contract-expr]
->i 컨트랙트 조합자는 각 인자와 결과에 이름이 있고, 그 이름을 하위 컨트랙트와 전/후 조건 절에서 사용할 수 있다는 점에서 ->* 조합자와 달라요. 다시 말해 ->i는 인자와 결과 사이의 의존성을 표현합니다.
->i의 선택적 첫 번째 키워드 인자는 결과 컨트랙트가 chaperone이 될지 나타내요. #:chaperone이면 인자와 결과에 대한 모든 컨트랙트가 chaperone contracts여야 하고 ->i의 결과는 chaperone contract가 됩니다. 없으면 결과 컨트랙트는 chaperone contract가 되지 않아요.
->i 컨트랙트의 첫 번째 하위 폼은 필수 인자를, 두 번째 하위 폼은 선택 인자를 다룹니다. 이어서 선택적 rest 인자 컨트랙트와 선택적 전제 조건(pre-condition)이 옵니다. 전제 조건은 #:pre 키워드로 도입되며 그 뒤에 의존하는 이름 목록이 옵니다. #:pre/name 키워드를 사용하면 제공된 문자열이 오류 메시지의 일부로 사용됩니다. #:post/name도 마찬가지예요. #:pre/desc나 #:post/desc를 사용하면 표현식의 결과는 ->*와 같은 방식으로 취급됩니다.
전제 조건 다음에는 선택적 param-value 비터미널이 오는데, 함수의 다이내믹 범위 동안 할당될 매개변수를 지정해요. 각 할당은 #:param 키워드로 도입되며, 그 뒤에 의존하는 이름 목록, 설정할 매개변수를 결정하는 param-expr, 그리고 그 매개변수와 연관될 val-expr이 옵니다.
dependent-range 비터미널은 가능한 결과 컨트랙트를 지정해요. any이면 어떤 값도 허용됩니다. 그렇지 않으면 결과 컨트랙트는 이름과 컨트랙트를 짝짓거나, 이름과 컨트랙트가 있는 여러 값 반환을 짝짓습니다. 마지막 두 경우에는 레인지 컨트랙트 다음에 선택적으로 후조건(post-condition)이 올 수 있는데, 레인지 컨트랙트가 any이면 후조건 표현식은 허용되지 않습니다. 전제 조건처럼 후조건도 의존하는 변수를 지정해야 해요.
이 샘플 컨트랙트를 보세요:
(->i ([x number?]
[y (x) (>=/c x)])
[result (x y) (and/c number? (>=/c (+ x y)))])
이것은 두 인자를 가진 함수를 지정하는데, 둘 다 숫자예요. 두 번째 인자(y)에 대한 컨트랙트는 첫 번째 인자보다 커야 한다고 요구합니다. 결과 컨트랙트는 두 인자의 합보다 큰 숫자를 약속합니다. y에 대한 의존성 명세는 인자 컨트랙트가 첫 번째 인자의 값에 의존함을 알리지만, result에 대한 의존성 시퀀스는 컨트랙트가 두 인자 값 모두에 의존함을 나타냅니다. 일반적으로 빈 시퀀스는 시퀀스를 전혀 추가하지 않는 것과 (거의) 동등하지만, 전자가 후자보다 더 비쌉니다. x에 대한 컨트랙트는 다른 것에 의존하지 않으므로 어떤 의존성 시퀀스도, 심지어 ()조차 오지 않습니다.
이 예는 앞의 예와 같지만, x와 y 인자가 이제 필수 위치 인자 대신 선택 키워드 인자라는 점이 달라요:
(->i ()
(#:x [x number?]
#:y [y (x) (>=/c x)])
[result (x y)
(and/c number?
(if (and (number? x) (number? y))
(>=/c (+ x y))
any/c))])
x나 y를 검사하는 레인지의 조건문은, 호출 문맥이 x나 y를 공급하지 않는 상황(그들이 the-unsupplied-arg에 바인딩될 수 있다는 뜻)을 다루기 위해 필요해요.
컨트랙트 표현식은 항상 순서대로 평가되지는 않습니다. 첫째, 어떤 컨트랙트 표현식에 의존성이 없으면 그 컨트랙트 표현식은 함수가 호출되거나 반환될 때가 아니라 ->i 표현식이 평가될 때 평가됩니다. 이런 무의존 컨트랙트 표현식은 나열된 순서대로 평가됩니다. 둘째, 의존 컨트랙트 하위 표현식은 계약된 함수가 호출되거나 반환될 때 의존성을 만족하는 어떤 순서로 평가됩니다. 즉, 어떤 인자에 대한 컨트랙트가 다른 컨트랙트의 값에 의존하면 전자가 먼저 평가됩니다(그래서 컨트랙트가 검사된 인자가 다른 쪽에서 사용 가능해짐). 두 인자 사이(또는 결과와 인자 사이)에 의존성이 없으면 소스 텍스트에서 먼저 나타나는 컨트랙트가 먼저 평가됩니다.
의존성을 가진 레인지 컨트랙트의 모든 식별자 위치가 _(밑줄)이면, 레인지 컨트랙트 표현식은 함수가 반환될 때가 아니라 호출될 때 평가됩니다. 그렇지 않으면 의존 레인지 표현식은 함수가 반환될 때 평가됩니다.
공급되지 않은 선택 인자가 있으면 대응하는 변수는 the-unsupplied-arg 값이라는 특수 값에 바인딩됩니다. 예를 들어 이 컨트랙트에서:
(->i ([x (y) (if (unsupplied-arg? y)
real?
(>=/c y))])
([y real?])
[any])
x에 대한 컨트랙트는 y에 의존하지만, y는 호출 지점에서 공급되지 않을 수 있어요. 그 경우 x에 대한 컨트랙트의 y 값은 the-unsupplied-arg이고, ->i 컨트랙트는 그것을 검사하고 y가 공급되지 않은 것을 반영해 x에 대한 컨트랙트를 조정해야 해요.
공급되지 않은 인자에 대한 컨트랙트 표현식이 의존적이고, 그 인자가 호출 지점에서 공급되지 않으면 그 컨트랙트 표현식은 전혀 평가되지 않습니다. 예를 들어 이 컨트랙트에서 y의 컨트랙트 표현식은 y가 공급될 때만 평가됩니다:
(->i ()
([x real?]
[y (x) (>=/c x)])
[any])
반면 x의 표현식은 항상 평가됩니다(실제로 의존성이 없으므로 ->i 표현식이 평가될 때 평가됩니다).
버전 8.7.0.1의 package base에서 변경됨: #:param 추가.
(->d (mandatory-dependent-dom ...) dependent-rest pre-condition dependent-range post-condition)(->d (mandatory-dependent-dom ...) (optional-dependent-dom ...) dependent-rest pre-condition dependent-range post-condition)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-condition | = | #:pre boolean-expr | | #:pre-cond boolean-expr dependent-range | = | any | | [_ range-expr] | | (values [_ range-expr] ...) | | [id range-expr] | | (values [id range-expr] ...) post-condition | = | #:post-cond boolean-expr
이 컨트랙트는 하위 호환성을 위해 여기에 있어요. 새 코드는 ->i를 사용해야 합니다.
이 컨트랙트는 ->i와 유사하지만, "느슨(lax)"해서 내부적으로 컨트랙트를 강제하지 않아요. 예를 들어 이 컨트랙트를 사용하면:
(->d ([f (-> integer? integer?))]
#:pre
(zero? (f #f))
[any])
f가 #f와 함께 호출되도록 허용해서, f의 컨트랙트가 정수만 받아들이도록 고집함으로써 f의 저자가 막으려 했던 잘못된 동작을 일으킵니다.
#:pre-cond와 #:post-cond 키워드는 #:pre와 #:post의 별칭이며 하위 호환성을 위해 제공됩니다.
(case-> (-> dom-expr ... rest range) ...)rest | = | #:rest rest-expr range | = | range-expr | | (values range-expr ...) | | any
이 컨트랙트 폼은 case-lambda와 일치하도록 설계되었어요. case->의 각 인자는 case-lambda의 한 절을 지배하는 컨트랙트예요. #:rest 키워드가 있으면 대응하는 절은 임의 개수의 인자를 받아야 합니다. 레인지 명세는 ->와 ->*와 같아요.
예를 들어 이 컨트랙트는 두 가지 경우를 가진 함수와 일치합니다. 하나는 정수를 받아 void를 반환하고, 다른 하나는 인자를 받지 않고 정수를 반환해요.
(case-> (-> integer? void?)
(-> integer?))
이런 컨트랙트는 단일 공유 정수에 대한 접근을 제어하는 함수를 보호하는 데 쓸 수 있어요.
(dynamic->* [#:mandatory-domain-contracts mandatory-domain-contracts #:optional-domain-contracts optional-domain-contracts #:mandatory-keywords mandatory-keywords #:mandatory-keyword-contracts mandatory-keyword-contracts #:optional-keywords optional-keywords #:optional-keyword-contracts optional-keyword-contracts #:rest-contract rest-contract] #:range-contracts range-contracts) → contract?mandatory-domain-contracts : (listof contract?) = '() optional-domain-contracts : (listof contract?) = '() mandatory-keywords : (listof keyword?) = '() mandatory-keyword-contracts : (listof contract?) = '() optional-keywords : (listof keyword?) = '() optional-keyword-contracts : (listof contract?) = '() rest-contract : (or/c #f contract?) = #f range-contracts : (or/c #f (listof contract?))
->*와 같지만, 인자와 결과의 개수가 컴파일 시간에 고정되는 대신 런타임에 계산될 수 있어요. #:range-contracts 인자로 #f를 전달하면 ->나 ->*에서 any를 사용한 것 같은 컨트랙트를 만들어요.
많은 용도에서 dynamic->*의 결과는 ->*(또는 ->)보다 느리지만, 일부에서는 비슷한 속도를 가져요. dynamic->*가 반환하는 컨트랙트의 이름은 -> 또는 ->* 구문을 사용합니다.
(unconstrained-domain-> range-expr ...)
함수를 받지만 함수의 도메인에는 제약을 두지 않는 컨트랙트를 만듭니다. range-expr들은 결과의 개수와 각 결과에 대한 컨트랙트를 결정합니다.
일반적으로 이 컨트랙트는 다른 컨트랙트와 결합되어, 도메인이 실제로 함수 자신을 안전하게 호출할 수 있는 것으로 알려져 있음을 보장해야 해요.
예를 들어 컨트랙트
(provide
(contract-out
[f (->d ([size natural-number/c]
[proc (and/c (unconstrained-domain-> number?)
(lambda (p)
(procedure-arity-includes? p size)))])
()
[[_ number?]])]))
는 함수 f가 자연수와 함수를 받는다고 말해요. f가 받는 함수의 도메인은 size 인자에 대한 경우를 포함해야 하는데, 이는 f가 자신의 입력에 size 인자를 안전하게 공급할 수 있다는 뜻입니다.
예를 들어 다음은 위 컨트랙트로 비난받을 수 없는 f의 정의예요:
(define (f i g)
(apply g (build-list i add1)))
predicate/c : contract?
(-> any/c boolean?)와 동등합니다. 이전에는 이 컨트랙트가 ->에 포함되지 않은 추가 최적화를 포함했기 때문에 필요했어요. 그러나 이제 ->도 같은 최적화를 수행하므로 이 컨트랙트는 더 이상 사용하면 안 됩니다. 이 컨트랙트는 여전히 하위 호환성을 위해 제공됩니다.
the-unsupplied-arg : unsupplied-arg?
호출 지점이 공급하지 않은 선택 인자를 바인딩하기 위해 ->i(그리고 ->d)가 사용합니다.
(unsupplied-arg? v) → boolean?v : any/c
v가 the-unsupplied-arg인지 판별하는 술어예요.