R6RS Scheme: 동등 술어

R6RS Scheme: 동등 술어

객체 간의 동등 관계를 검사하는 eqv?, eq?, equal? 술어를 설명해요. 각 술어가 무엇을 비교하고 서로 어떻게 다른지, 그리고 각각 언제 #t·#f·미지정 값을 반환하는지 규칙과 예시로 다뤄요.

출처: 문서

본문

술어(predicate)는 항상 불리언 값(#t 또는 #f)을 반환하는 프로시저예요. 동등 술어(equivalence predicate)는 수학적 동등 관계의 계산적 유사체예요(그것은 대칭적이고, 반사적이며, 이행적이에요). 이 절에서 설명하는 동등 술어들 중 eq?는 가장 정밀한, 즉 가장 잘 식별하는 것이고, equal?는 가장 거친 것이에요. eqv? 술어는 eq?보다 약간 덜 식별적이에요.

(eqv? obj1 obj2) — procedure

eqv? 프로시저는 객체들에 대한 유용한 동등 관계를 정의해요. 간단히 말하면, obj1obj2가 보통 같은 객체로 간주되어야 하면 #t를, 그렇지 않으면 #f를 반환해요. 이 관계는 해석의 여지가 약간 남아 있지만, eqv?에 대한 다음의 부분 명세는 모든 구현에서 성립해야 해요.

eqv? 프로시저는 다음 중 하나가 성립하면 #t를 반환해요:

  • obj1obj2가 둘 다 불리언이고 boolean=? 프로시저(11.8절)에 따라 같아요.
  • obj1obj2가 둘 다 기호이고 symbol=? 프로시저(11.10절)에 따라 같아요.
  • obj1obj2가 둘 다 정확한 수치 객체이고 수치적으로 같아요(=, 11.7절 참조).
  • obj1obj2가 둘 다 부정확한 수치 객체이고, 수치적으로 같으면서(=, 11.7절 참조), Scheme의 표준 산술 프로시저들의 유한 합성으로 정의될 수 있는 다른 어떤 프로시저에 인자로 전달될 때 eqv?의 의미에서 같은 결과를 산출해요.
  • obj1obj2가 둘 다 문자이고 char=? 프로시저(11.11절)에 따라 같은 문자예요.
  • obj1obj2가 둘 다 빈 리스트예요.
  • obj1obj2가 저장소(store, 5.10절)의 같은 위치를 가리키는 객체들인데, 예를 들어 쌍, 벡터, 바이트벡터("Bytevectors" 라이브러리 장), 문자열, 해시테이블("Hash tables" 라이브러리 장), 레코드("Records" 라이브러리 장), 포트("Port I/O" 라이브러리 절) 따위예요.
  • obj1obj2가 "Procedural layer" 라이브러리 절에서 eqv?인 것으로 명시된 레코드 타입 기술자(record-type descriptor)예요.

eqv? 프로시저는 다음 중 하나가 성립하면 #f를 반환해요:

  • obj1obj2가 다른 타입(11.1절)이에요.
  • obj1obj2boolean=? 프로시저가 #f를 반환하는 불리언들이에요.
  • obj1obj2symbol=? 프로시저가 #f를 반환하는 기호들이에요.
  • obj1obj2 중 하나는 정확한 수치 객체인데 다른 하나는 부정확한 수치 객체예요.
  • obj1obj2= 프로시저가 #f를 반환하는 유리수 수치 객체들이에요.
  • obj1obj2가 Scheme의 표준 산술 프로시저들의 유한 합성으로 정의될 수 있는 다른 어떤 프로시저에 인자로 전달될 때 eqv?의 의미에서 다른 결과를 산출해요.
  • obj1obj2char=? 프로시저가 #f를 반환하는 문자들이에요.
  • obj1obj2 중 하나는 빈 리스트인데 다른 하나는 아니에요.
  • obj1obj2가 서로 다른 위치를 가리키는 객체들인데, 예를 들어 쌍, 벡터, 바이트벡터("Bytevectors" 라이브러리 장), 문자열, 레코드("Records" 라이브러리 장), 포트("Port I/O" 라이브러리 절), 해시테이블("Hashtables" 라이브러리 장) 따위예요.
  • obj1obj2가 쌍, 벡터, 문자열, 레코드 또는 해시테이블인데, 둘 다에 같은 접근자(즉 car, cdr, vector-ref, string-ref, 또는 레코드 접근자)를 적용했을 때 eqv?#f를 반환하는 결과를 산출해요.
  • obj1obj2가 어떤 인자들에 대해 다르게 동작할(다른 값을 반환하거나 다른 부수 효과를 가질) 프로시저들이에요.

Note: obj1obj2가 수치 객체일 때 eqv? 프로시저가 #t를 반환한다고 해서 =obj1obj2를 인자로 호출할 때 #t를 반환한다는 뜻은 아니에요.

(eqv? 'a 'a)                     ⇒ #t
(eqv? 'a 'b)                      ⇒ #f
(eqv? 2 2)                        ⇒ #t
(eqv? '() '())                    ⇒ #t
(eqv? 100000000 100000000)        ⇒ #t
(eqv? (cons 1 2) (cons 1 2))      ⇒ #f
(eqv? (lambda () 1)
      (lambda () 2))              ⇒ #f
(eqv? #f 'nil)                    ⇒ #f

다음 예들은 위의 규칙들이 eqv?의 동작을 완전히 명시하지 않는 경우를 보여줘요. 그러한 경우에 대해 말할 수 있는 전부는, eqv?가 반환하는 값이 불리언이어야 한다는 것이에요.

(let ((p (lambda (x) x)))
  (eqv? p p))                    ⇒ unspecified
(eqv? "" "")             ⇒ unspecified
(eqv? '#() '#())         ⇒ unspecified
(eqv? (lambda (x) x)
      (lambda (x) x))    ⇒ unspecified
(eqv? (lambda (x) x)
      (lambda (y) y))    ⇒ unspecified
(eqv? +nan.0 +nan.0)             ⇒ unspecified

다음 예시 세트는 지역 상태를 가진 프로시저들과 함께 eqv?를 사용하는 것을 보여줘요. gen-counter에 대한 호출은 각 프로시저가 자기만의 내부 카운터를 가지므로 매번 서로 다른 프로시저를 반환해야 해요. gen-loser에 대한 호출은 호출될 때 동등하게 동작하는 프로시저들을 반환해요. 그러나 eqv?는 이 동등성을 검출하지 못할 수 있어요.

(define gen-counter
  (lambda ()
    (let ((n 0))
      (lambda () (set! n (+ n 1)) n))))

(let ((g (gen-counter)))
  (eqv? g g))           ⇒ unspecified
(eqv? (gen-counter) (gen-counter))
                        ⇒ #f

(define gen-loser
  (lambda ()
    (let ((n 0))
      (lambda () (set! n (+ n 1)) 27))))

(let ((g (gen-loser)))
  (eqv? g g))            ⇒ unspecified
(eqv? (gen-loser) (gen-loser))
                         ⇒ unspecified

(letrec ((f (lambda () (if (eqv? f g) 'both 'f)))
         (g (lambda () (if (eqv? f g) 'both 'g))))
  (eqv? f g))  ⇒ unspecified

(letrec ((f (lambda () (if (eqv? f g) 'f 'both)))
         (g (lambda () (if (eqv? f g) 'g 'both))))
  (eqv? f g))  ⇒ #f

구현들은 적절한 곳에서 상수들 사이의 구조를 공유할 수 있어요. 게다가, 상수는 11.4.1절에서 언급한 것처럼 서로 다른 위치 집합들에 동시에 존재하도록 구현에 의해 언제든지 복사될 수 있어요. 따라서 상수들에 대한 eqv?의 값은 때때로 구현 의존이에요.

(eqv? '(a) '(a))                 ⇒ unspecified
(eqv? "a" "a")                   ⇒ unspecified
(eqv? '(b) (cdr '(a b)))         ⇒ unspecified
(let ((x '(a)))
  (eqv? x x))                    ⇒ #t

(eq? obj1 obj2) — procedure

eq? 술어는 eqv?와 비슷하되, 어떤 경우에는 eqv?가 검출할 수 있는 것보다 더 미세한 구별들을 식별할 수 있다는 점이 달라요.

eq?eqv? 술어는 기호, 불리언, 빈 리스트, 쌍, 프로시저, 비-빈 문자열, 바이트벡터, 벡터, 레코드에 대해 같은 동작을 가지는 것이 보장돼요. 수치 객체와 문자에 대한 eq?의 동작은 구현 의존이지만, 그것은 항상 #t 또는 #f를 반환하고, eqv?#t를 반환할 때에만 #t를 반환해요. eq? 술어는 빈 벡터, 빈 바이트벡터, 빈 문자열에서 eqv?와 다르게 동작할 수도 있어요.

(eq? 'a 'a)                     ⇒ #t
(eq? '(a) '(a))                 ⇒ unspecified
(eq? (list 'a) (list 'a))        ⇒ #f
(eq? "a" "a")                    ⇒ unspecified
(eq? "" "")                      ⇒ unspecified
(eq? '() '())                    ⇒ #t
(eq? 2 2)                        ⇒ unspecified
(eq? #\A #\A)  ⇒ unspecified
(eq? car car)                    ⇒ #t
(let ((n (+ 2 3)))
  (eq? n n))       ⇒ unspecified
(let ((x '(a)))
  (eq? x x))       ⇒ #t
(let ((x '#()))
  (eq? x x))       ⇒ unspecified
(let ((p (lambda (x) x)))
  (eq? p p))       ⇒ unspecified

(equal? obj1 obj2) — procedure

equal? 술어는 (어쩌면 무한한) 인자들의 규칙 트리로의 전개가 정렬된 트리로서 같을 때 그리고 오직 그때 #t를 반환해요.

equal? 술어는 쌍과 벡터를 나가는 변(outgoing edge)을 가진 노드로 취급하고, 문자열을 비교하는 데 string=?를, 바이트벡터를 비교하는 데 bytevector=?를 사용하며("Bytevectors" 라이브러리 장 참조), 다른 노드들을 비교하는 데 eqv?를 사용해요.

(equal? 'a 'a)                  ⇒ #t
(equal? '(a) '(a))               ⇒ #t
(equal? '(a (b) c)
        '(a (b) c))              ⇒ #t
(equal? "abc" "abc")             ⇒ #t
(equal? 2 2)                     ⇒ #t
(equal? (make-vector 5 'a)
        (make-vector 5 'a))      ⇒ #t
(equal? '#vu8(1 2 3 4 5)
        (u8-list->bytevector
         '(1 2 3 4 5))           ⇒ #t
(equal? (lambda (x) x)
        (lambda (y) y))   ⇒ unspecified
(let* ((x (list 'a))
       (y (list 'a))
       (z (list x y)))
  (list (equal? z (list y x))
        (equal? z (list x x))))             
  ⇒ (#t #t)

Note: equal? 프로시저는 인자들이 사이클을 포함하더라도 항상 종료해야 해요.

더 알아보기 (Learn more)

출처: Equivalence predicates - R6RS