rokdocs
v0.1

2.5. 비교 트레잇

rok::core::cmp 모듈은 값 사이의 비교를 정의하는 트레잇 패밀리입니다. ==, !=, <, >, <=, >= 연산자뿐 아니라 해시, 정렬, 패턴 매칭이 모두 이 장의 트레잇을 통해 dispatch 됩니다.

rok 은 비교를 4-트레잇 위계 (PartialEq / Eq / PartialOrd / Ord) 와 별도의 Hash 트레잇으로 분리합니다. 이렇게 분리하는 이유는 IEEE 754 부동소수의 NaN 처리와 해시 일관성 보장이라는 두 가지 실용 요구를 동시에 만족시키기 위함입니다.

2.5.1. 4 트레잇 위계 - partial 이 필요한 이유

위계 구조는 두 가지 관점에서 동시에 정리할 수 있습니다.

집합 관점 - A ⊃ B 는 "A 를 만족하는 타입의 집합이 B 를 만족하는 타입의 집합을 포함" 한다는 뜻입니다. 즉 A 가 더 일반적이고 B 가 더 specific 입니다.

PartialEq  ⊃  Eq           // PartialEq 인 타입의 집합 ⊇ Eq 인 타입의 집합
PartialOrd ⊃  Ord          //  (모든 Eq 는 PartialEq 이지만 그 역은 아님)
PartialEq  ⊃  PartialOrd

함의 관점 (supertrait) - B ⇒ A 는 "B 를 구현하면 A 도 자동으로 요구된다" 는 뜻이며, rok 코드로는 trait B: A 로 적습니다. 이때 A 가 B 의 supertrait 입니다.

trait Eq: PartialEq { ... }                    // PartialEq 가 Eq 의 supertrait
trait PartialOrd: PartialEq { ... }            // PartialEq 가 PartialOrd 의 supertrait
trait Ord: PartialOrd + Eq { ... }             // PartialOrd 와 Eq 가 Ord 의 supertrait

두 관점은 정확히 같은 사실을 다른 각도에서 표현합니다. B ⇒ A 면 B 인 타입은 모두 A 이기도 하므로 A ⊃ B 가 자연스럽게 따라옵니다. 즉 supertrait 가 집합 관점에서는 "더 큰 쪽" 입니다.

Hash 트레잇은 위 위계와 별개의 줄에 위치합니다.

Eq ⊃ Hash         // Eq 인 타입 중 일부만 Hash - Hash 는 Eq 와의 일관성을 약속

partial 이 따로 분리되는 이유는 IEEE 754 floating point 의 NaN 때문입니다.

let a: f64 = 0.0 / 0.0;       // NaN
let b: f64 = 0.0 / 0.0;       // NaN
a == b;                       //→ false (NaN != NaN by IEEE 754)
a < b;                        //→ false
a > b;                        //→ false
a <= b;                       //→ false

NaN 은 자기 자신과도 같지 않은 값입니다. 이는 reflexivity 위배에 해당하며 a == a 가 보장되지 않습니다. 따라서 타입별 트레잇 구현이 다음과 같이 갈라집니다. f32 와 f64 는 PartialEq, PartialOrd 만 구현되며, i32, bool, str 등은 Eq, Ord 까지 구현되어 총 순서를 만족합니다.

이러한 분리 덕분에 Hash, HashMap, BTreeMap, sort 알고리듬에서 Hash 의 일관성 약속과 total ordering 보장이 안전하게 유지됩니다.

2.5.2. PartialEq - 부분 동등성 (==, !=)

부분 동등성은 다음과 같이 정의됩니다.

#[lang("eq_trait")]
trait PartialEq<Rhs = Self> {
    #[infix("==")]
    #[infix("≡")]    // Unicode alias
    fn eq(self, rhs: Rhs) -> bool;

    #[infix("!=")]
    #[infix("≠")]    // Unicode alias
    fn ne(self, rhs: Rhs) -> bool {
        !self.eq(rhs)                // default impl
    }
}

PartialEq 가 보장하는 약속 은 Symmetric 성질로서 a == b 와 b == a 가 동치이며, Transitive 성질로서 a == b 와 b == c 가 성립하면 a == c 도 성립한다는 점입니다. 다만 reflexivity (a == a) 는 IEEE 754 NaN 때문에 보장되지 않습니다.

default ne

ne 는 기본적으로 !self.eq(rhs) 로 구현됩니다. 사용자가 성능 최적화 등을 위해 별도로 구현하는 것도 가능합니다.

사용 예

let r1 = 5i32 == 5i32;     //→ true
let r2 = "hi" != "hello";  //→ true

let nan: f64 = f64::NAN;
let r3 = nan == nan;       //→ false  (PartialEq 의 부분성)

2.5.3. Eq - 완전 동등성

완전 동등성은 marker trait 으로 정의됩니다.

trait Eq: PartialEq<Self> {
    // marker trait - 추가 메서드 없음
}

Eq 는 marker trait 이며, 메서드 없이 reflexivity 약속만 추가합니다.

Eq 가 추가하는 약속 은 Reflexive 성질, 즉 a == a 가 항상 true 라는 점입니다. 이 약속이 있어야 HashMap 의 키, BTreeMap 의 키, sort 알고리듬이 정상적으로 작동합니다.

impl 정책 은 다음과 같이 정리됩니다. i32, i64, u8..u64, bool, char, c_char, str 등 모든 정수 / 부울 / 문자 / 문자열 타입은 Eq 를 구현합니다. 반면 f32, f64, f64 | none 등 부동소수 또는 부동소수를 포함한 합 타입은 Eq 를 구현하지 않습니다. 사용자 struct 또는 enum 의 #[derive(Eq)] 는 모든 필드가 Eq 일 때만 가능합니다.

let n: i32 = 42;
n == n;       //→ true (Eq 보장)

2.5.4. PartialOrd - 부분 순서 (<, >, <=, >=)

부분 순서 트레잇은 비교 연산자들을 정의합니다.

#[lang("cmp_trait")]
trait PartialOrd<Rhs = Self>: PartialEq<Rhs> {
    #[infix("<")]
    fn lt(self, rhs: Rhs) -> bool;

    #[infix(">")]
    fn gt(self, rhs: Rhs) -> bool;

    #[infix("<=")]
    #[infix("≤")]    // Unicode alias
    fn le(self, rhs: Rhs) -> bool;

    #[infix(">=")]
    #[infix("≥")]    // Unicode alias
    fn ge(self, rhs: Rhs) -> bool;

    fn partial_cmp(self, rhs: Rhs) -> Ordering | none {
        // default impl - lt/gt/eq 조합
        if self.eq(rhs) { Ordering::Equal }
        else if self.lt(rhs) { Ordering::Less }
        else if self.gt(rhs) { Ordering::Greater }
        else { none }                     // NaN vs NaN 등 - 비교 불가
    }
}

이 트레잇이 보장하는 약속 은 다음과 같습니다. Antisymmetric 성질로서 a < b 이면 !(b < a) 가 성립하며, Transitive 성질로서 정의된 비교에 한해 a < b 와 b < c 가 성립하면 a < c 도 성립합니다. 다만 Trichotomy 는 부분적으로만 성립하므로 두 값 사이 순서가 항상 정의되지는 않으며, NaN 같은 incomparable 값이 존재합니다.

partial_cmp 는 기본 구현이 있는 (provided) 메서드이며 Ordering | none 을 반환합니다. 비교 불가능한 경우 (NaN 등) 에는 none 입니다.

Ordering 타입

3-way 비교의 결과는 다음 enum 으로 표현됩니다.

enum Ordering {
    Less,
    Equal,
    Greater,
}

Ord::cmp 는 Ordering 을 직접 반환합니다. Ordering 은 Debug 를 구현합니다 (Less / Equal / Greater).

비교 체이닝 거부

refs-lang 3.2.5 가 정의하는 바에 따르면 비결합 트레잇 chain 은 거부됩니다.

a < b < c;        // 컴파일 에러 - 비결합 (none) 트레잇 chain 거부

대안으로는 명시적 AND 를 사용합니다.

a < b && b < c;   // 명시적 AND

2.5.5. Ord - 완전 순서

완전 순서 트레잇은 다음과 같이 정의됩니다.

#[lang("ord_trait")]
trait Ord: Eq + PartialOrd<Self> {
    fn cmp(self, rhs: Self) -> Ordering;

    // default - Ord 가 정의된 두 값은 항상 비교 가능
    fn min(self, rhs: Self) -> Self {
        if self.le(rhs) { self } else { rhs }
    }

    fn max(self, rhs: Self) -> Self {
        if self.ge(rhs) { self } else { rhs }
    }

    fn clamp(self, low: Self, high: Self) -> Self {
        if self.lt(low) { low }
        else if self.gt(high) { high }
        else { self }
    }
}

Ord 가 추가하는 약속 은 Total 성질로서 모든 두 값 사이의 순서가 Less, Equal, Greater 셋 중 하나로 정의된다는 점입니다. 따라서 partial_cmp 가 none 을 반환하지 않으며, 항상 Ordering 값을 돌려줍니다.

이 보장이 있어야 sort 알고리듬이 정상적으로 작동합니다.

impl 정책 을 살펴보면 i32, i64, u*, bool, char, c_char, str 은 Ord 를 구현합니다. 반면 f32, f64 는 NaN 때문에 incomparable 한 경우가 발생하므로 Ord 를 구현하지 않습니다.

min / max / clamp

3 개의 default 메서드가 제공되므로, impl 측이 별도로 구현하지 않아도 자동으로 사용할 수 있습니다.

let r = 3i32.min(7i32);              //→ 3
let m = 5.max(10);                   //→ 10
let c = 100.clamp(0, 50);            //→ 50
let nan_safe = 1.0f64.clamp(0.0, 10.0);  // ERROR - f64 는 Ord 가 정의되지 않음
                                          // (NaN 안전 clamp 는 별도 메서드 사용)

2.5.6. Hash - 해시 (Eq 일관성 약속)

해시 트레잇은 다음과 같이 정의됩니다.

#[lang("hash_trait")]
trait Hash: Eq {
    fn hash(self) -> u64;
}

핵심 약속 은 다음과 같습니다.

a == b ⇒ a.hash() == b.hash()

이 약속이 HashMap<K, V>, HashSet<K> 의 모든 알고리듬 정확성을 뒷받침합니다.

Hash: Eq 이므로 Eq 를 구현하지 않은 타입은 Hash 도 구현할 수 없습니다. 따라서 f64 는 Eq 를 구현하지 않으므로 Hash 도 할 수 없습니다.

사용 예

use rok::core::cmp::Hash;

let h1 = "hello".hash();
let h2 = "hello".hash();
h1 == h2;            //→ true (Hash 일관성)

let h3 = 42i32.hash();

사용자 정의

#[derive(PartialEq, Eq, Hash)]
struct UserId { id: u64; }

// 자동 derive - 모든 필드 (id: u64) 가 Eq+Hash 라 가능

충돌 (collision)

같은 타입의 서로 다른 두 값이 같은 hash 를 반환할 수 있습니다. 즉 a != b 인데도 a.hash() == b.hash() 인 경우가 허용됩니다. 이는 버그가 아니며, 64-bit hash 공간에서는 충돌이 불가피합니다. 충돌 처리는 HashMap 등의 자료구조가 담당합니다.

2.5.7. 구현 매트릭스

각 타입별 트레잇 구현 현황은 다음 표와 같이 정리됩니다.

타입PartialEqEqPartialOrdOrdHash동작
i*, u*✓✓✓✓✓rok.eq.OP, rok.lt.OP, ...
f32, f64✓✗✓✗✗rok.eq.f64, rok.lt.f64
bool✓✓✓✓✓rok.eq.bool, rok.lt.bool
char, c_char✓✓✓✓✓rok.eq.char, ...
str✓✓✓✓✓lexicographic
Tuple<...>✓ (모든 부분 PartialEq 시)✓✓✓✓lexicographic, 1-12 항
[T; N]✓ (T:PartialEq)✓✓✓✓lexicographic
List<T>✓ (T:PartialEq 시)✓✓✓✓lexicographic
Dict<K,V>✓ (K,V:PartialEq)✓✗✗✓순서 무관 비교 / 해시
Set<T>✓ (T:PartialEq)✓✗✗✓순서 무관 비교 / 해시
Range<T>✓ (T:PartialEq)............start/end 비교
합 A | B✓ (모든 항이 구현 시)✓✓✓✓항 순서 → payload

Dict 와 Set 은 순서 없는 컨테이너이므로 PartialOrd / Ord 가 없습니다. PartialEq / Eq / Hash 는 원소 (키와 값) 가 그 트레잇을 구현하면 구현되며, 순회 순서와 무관하게 비교하고 해시합니다 (2.9).

List 는 사전식으로 비교합니다. 앞에서부터 원소를 비교해 처음 다른 원소가 순서를 정하고, 한쪽이 다른 쪽의 접두사이면 짧은 쪽이 작습니다.

합 타입의 두 값은 먼저 항 (변형) 의 순서 로, 같은 항이면 그 항 타입의 Ord 로 payload 를 비교합니다. 항의 순서는 합 타입의 정규 순서 (refs-lang 2.6.4) 이며, 적힌 순서와 무관합니다. none 은 늘 마지막이므로 T | none 에서 none 은 어떤 T 값보다 큽니다. min / max / clamp 도 같은 순서를 따릅니다.

2.5.8. broadcast 비교 트레잇

2.6 broadcast 의 19 개 broadcast 트레잇 중 비교에 해당하는 6 개는 다음과 같이 정의됩니다.

trait DotEq<Rhs>  { #[infix(".==")] fn dot_eq(...) -> Output; }
trait DotNe<Rhs>  { #[infix(".!=")] fn dot_ne(...) -> Output; }
trait DotLt<Rhs>  { #[infix(".<")] fn dot_lt(...) -> Output; }
trait DotGt<Rhs>  { #[infix(".>")] fn dot_gt(...) -> Output; }
trait DotLe<Rhs>  { #[infix(".<=")] fn dot_le(...) -> Output; }
trait DotGe<Rhs>  { #[infix(".>=")] fn dot_ge(...) -> Output; }

각 트레잇은 List 나 Vec3 등 컨테이너의 element-wise 비교를 담당하며, 결과 타입 (Output) 은 보통 List<bool> 또는 [bool; N] 이 됩니다.

자세한 broadcast 의미는 2.6 broadcast 를 참조합니다.

2.5.9. #[derive(Eq, Ord, Hash, ...)]

refs-lang 6.2 #[derive] 가 derive 메커니즘을 정의합니다. cmp 트레잇 5 개는 모두 derive 가 가능합니다.

#[derive(PartialEq, Eq, PartialOrd, Ord, Hash)]
struct Version { major: u32; minor: u32; patch: u32; }

derive 동작은 트레잇별로 다음과 같이 진행됩니다. PartialEq 와 Eq 의 경우 모든 필드 동등성의 AND 로 정의됩니다. PartialOrd 와 Ord 는 필드 선언 순서에 따른 lexicographic 비교로 동작합니다. Hash 는 모든 필드 hash 를 결합해 계산하며, 결합 함수는 사양으로 정하지 않습니다.

요구 조건은 모든 필드가 (재귀적으로) 같은 트레잇을 구현해야 한다는 점이며, Eq / Ord 는 상위 트레잇 (PartialEq / PartialOrd, Eq) 도 함께 derive 하거나 구현해야 합니다. 한 필드라도 Eq 를 구현하지 않으면 #[derive(Eq)] 는 컴파일 에러가 됩니다.

2.5.10. 패턴 매칭과 cmp

match arm 의 literal 패턴은 PartialEq::eq 를 사용합니다.

match x {
    0 => "zero",
    1 => "one",
    _ => "other",
}
// → 0.eq(x) → 비교, 1.eq(x) → 비교, ...

range 패턴 (0..=10 =>) 은 PartialOrd::le 와 ge 를 사용합니다.

자세한 패턴 의미는 refs-lang 3.11 패턴 을 참조합니다.

GitHub에서 이 페이지 고치기