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. 구현 매트릭스
각 타입별 트레잇 구현 현황은 다음 표와 같이 정리됩니다.
| 타입 | PartialEq | Eq | PartialOrd | Ord | Hash | 동작 |
|---|---|---|---|---|---|---|
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 패턴 을 참조합니다.