link
- Generics기초
- Summary (one-screen takeaway)
- 1.1. The simplest case:
TandImean different roles - 1.2. Returning
Tfrom anI - 1.3. Adding trait bounds to T
- 1.4. When I and T are constrained together
- 1.5. When I depends on T and something else
- 1.6. Mixing generics with associated types
- 1.7. Mixing generics with trait generics (
Sum<A>style) - 1.8. Higher-order mixing: passing a function
- 1.9. Why Rust forces explicit mixing
- 1.10. Mental model (very important)
- 1.1. The simplest case:
- 2.0 Rewrite all examples using only associated types
- (safe rust)안정한 러스트 코드를 작성하기 위한 기본 스킬들~~
- 3.0 Here’s the non-panicking version of your sum that returns
Option<I::Item>instead of panicking on an empty iterator
Please tell me in detail an example of mixing multiple types such as T and I when using generics in Rust language.
Below is a step-by-step, detailed explanation of how multiple generic parameters (T, I, etc.) are mixed together in real Rust code, with progressively more advanced examples.
- I’ll start simple and move toward iterator-heavy, trait-bound-heavy patterns—very close to what you’ve been exploring.
다음은 실제 Rust 코드에서 여러 일반 매개변수(T, I 등)가 어떻게 혼합되는지에 대한 단계별 상세한 설명이며, 점진적으로 더 발전된 예제를 제공합니다.
- 간단한 것부터 시작해서 반복자가 많고 특성이 결합된 패턴으로 넘어가겠습니다. 이 패턴은 여러분이 탐구해 온 것과 매우 유사합니다.
1.1 The simplest case: T and I mean different roles|🔝|
- A very common pattern:
fn process<I, T>(iter: I)
where
I: Iterator<Item = T>,
{
// ...
}- What each type means
| Generic | Role |
|---|---|
I | The iterator type (how we get values) |
T | The item type (what the values are) |
- Example call:
let v = vec![1, 2, 3];
process(v.into_iter());- Here the compiler infers:
I = std::vec::IntoIter<i32>
T = i32- Key idea:
I and T are orthogonal.
One describes iteration mechanics, the other data.
I와 T는 직교합니다.
하나는 반복 역학을 설명하고, 다른 하나는 데이터를 설명합니다.
1.2 Returning T from an I|🔝|
- This is the most common “mixing” pattern.
fn first<I, T>(iter: I) -> Option<T>
where
I: IntoIterator<Item = T>,
{
iter.into_iter().next()
}
Why both generics are needed
- You cannot write
fn first<T>(iter: T) - Because
Tis the item, not the iterator IandTmust be distinct
- You cannot write
두 제네릭이 모두 필요한 이유
- fn을 먼저 쓸 수 없습니다(iter: T)
T는 반복자가 아니라 항목이기 때문에I와T는 구별되어야 합니다
Usage:
let x = first(vec![10, 20, 30]);
assert_eq!(x, Some(10));1.3. Adding trait bounds to T|🔝|
- Now we mix:
- Structure (
I: Iterator) - Behavior (
T: Add,Clone, etc.)
- Structure (
use std::ops::Add;
fn sum<I, T>(iter: I) -> Option<T>
where
I: Iterator<Item = T>,
T: Add<Output = T>,
{
iter.reduce(|a, b| a + b)
}Why this separation matters
Iteratorknows nothing about+Addknows nothing about iteration- Rust composes them explicitly
이 분리가 중요한 이유
- ‘반복자’는 ’+‘에 대해 아무것도 모릅니다
- ‘추가’는 반복에 대해 아무것도 모릅니다
- 녹은 그것들을 명시적으로 구성합니다
1.4. When I and T are constrained together|🔝|
- 1.4.
I와T가 함께 제한될 때 - Sometimes
IandTare linked through bounds:- 때때로
I와T는 경계를 통해 연결됩니다:
- 때때로
fn debug_items<I, T>(iter: I)
where
I: Iterator<Item = T>,
T: std::fmt::Debug,
{
for item in iter {
println!("{:?}", item);
}
}- Usage:
debug_items(vec![1, 2, 3].iter());
debug_items(vec!["a", "b", "c"].iter());
// Sum examples
let nums = vec![1, 2, 3, 4, 5];
if let Some(total) = sum(nums.into_iter()) {
println!("Sum of nums: {}", total);
}
let floats = vec![1.5, 2.5, 3.0];
if let Some(total) = sum(floats.into_iter()) {
println!("Sum of floats: {}", total);
}Here:
IchangesTchanges- The function remains valid
여기:
- ‘I’의 변화
- ‘T’ 변경 사항
- 함수는 여전히 유효합니다
1.5. When I depends on T and something else|🔝|
1.5. ‘I’가 ‘T’와 다른 것에 의존할 때
Now we add another generic, often a reference type:
- 이제 또 다른 일반적인, 종종 참조 유형을 추가합니다:
fn collect_refs<'a, I, T>(iter: I) -> Vec<&'a T>
where
I: Iterator<Item = &'a T>,
{
iter.collect()
}- Call site
let v = vec![1, 2, 3];
let refs = collect_refs(v.iter());- Here:
I = std::slice::Iter<'_, i32>
T = i32
'a = lifetime of v- Key insight:
Lifetimes behave like hidden generics that interact with
IandT.
- 주요 인사이트:
Lifetimes은
I와T와 상호작용하는 숨겨진 제네릭처럼 행동합니다.
1.6. Mixing generics with associated types|🔝|
Now let’s rewrite using associated types instead of a separate
T.Version A: Separate T
fn sum<I, T>(iter: I) -> Option<T>
where
I: Iterator<Item = T>,
T: Add<Output = T>,
{
iter.reduce(|a, b| a + b)
}
- Version B: Associated type only
fn sum<I>(iter: I) -> Option<I::Item>
where
I: Iterator,
I::Item: Add<Output = I::Item>,
{
iter.reduce(|a, b| a + b)
}Why this is often preferred
- Fewer generic parameters
- Clear ownership: Item belongs to I
- Less type clutter
This is exactly the form you were exploring earlier.
이것이 자주 선호되는 이유
- 일반 매개변수 수 감소
- 명확한 소유권: 항목은 I 소유입니다
- 덜 유형화된 어수선함
이것이 바로 당신이 이전에 탐구했던 형태입니다.
1.7. Mixing generics with trait generics (Sum<A> style)|🔝|
- Now it gets interesting.
use std::iter::Sum;
fn total<I, T>(iter: I) -> T
where
I: Iterator,
T: Sum<I::Item>,
{
iter.sum()
}What’s happening
I::Item= input typeT= output type- They are not the same
Example:
let v = vec![1u8, 2, 3];
let x: u64 = total(v.into_iter().map(|x| x as u64));- Inference:
I::Item = u8
T = u64- This is impossible without mixing multiple generic parameters.
1.8. Higher-order mixing: passing a function|🔝|
fn map_reduce<I, T, F>(iter: I, f: F) -> Option<T>
where
I: Iterator<Item = T>,
F: Fn(T, T) -> T,
{
iter.reduce(f)
}- Now we have three generic parameters:
| Generic | Meaning |
|---|---|
I | iteration strategy |
T | data type |
F | combination logic |
1.9. Why Rust forces explicit mixing|🔝|
Rust does not do implicit typeclass lifting like Haskell.
(Haskell code)So instead of:
-- main.Hs
sum :: (Iterator i, Add t) => i t -> t- Rust requires:
fn sum<I>(iter: I) -> I::Item
where
I: Iterator,
I::Item: Add<Output = I::Item>,This explicitness:
- Improves error messages
- Prevents accidental constraints
- Makes APIs honest about assumptions
이 명확성:
- 오류 메시지를 개선합니다
- 우발적인 제약을 방지합니다
- 가정에 대한 API를 정직하게 만듭니다
1.10. Mental model (very important)|🔝|
Think in layers
- Container / flow →
I - Element / data → T or
I::Item - Behavior → trait bounds (
Add,Debug,Sum, etc.) - Relationships →
Item = T,T: Trait<U> - Generics in Rust describe relationships, not just types
- Container / flow →
층층이 쌓아 생각하기
- 컨테이너 / 흐름 →
I - 요소 / 데이터 → T 또는
I:Item - 행동 → 특성 경계(
Add,Debug,Sum, etc.) - 관계 →
Item = T,T: Trait<U> - 러스트의 제네릭은 유형뿐만 아니라 관계를 설명합니다
- 컨테이너 / 흐름 →
Summary (one-screen takeaway)|🔝|
| Pattern | Meaning |
|---|---|
I: Iterator<Item = T> | “I produces T” |
I::Item | “the item type of I” |
T: Add<Output = T> | “T can be combined” |
T: Sum<I::Item> | “T aggregates items” |
| Multiple generics | “multiple roles, explicitly named” |
- 한글 번역
| Pattern | Meaning |
|---|---|
I: Iterator<Item = T> | “T를 생성합니다” |
I::Item | “I 항목 유형” |
T: Add<Output = T> | “T를 결합할 수 있습니다” |
T: Sum<I::Item> | “T는 항목을 집계합니다” |
| Multiple generics | “여러 역할, 명시적으로 명명됨” |
2.0 Rewrite all examples using only associated types|🔝|
2.0 관련 유형만 사용하여 모든 예제 다시 쓰기
That means:
- No separate
Tgeneric - Always use
I::Item - Let the iterator own the item type
- No separate
I’ll go step by step, rewriting the earlier patterns and explaining why this is idiomatic Rust.
즉:
- 별도의
T제네릭 없음 - 항상
I::Item을 사용합니다 - 반복자가 항목 유형을 소유하도록 합니다
- 별도의
단계별로 진행하면서 이전 패턴을 다시 작성하고 이것이 관용적인 러스트인 이유를 설명하겠습니다.
0. Core idea (important)|🔝|
- Instead of writing:
fn f<I, T>(iter: I)
where
I: Iterator<Item = T>,fn f<I>(iter: I)
where
I: Iterator,- and refer to the item as:
I::Item- This uses associated types instead of extra generics.
2.1. first example (associated-type version)|🔝|
- Before (two generics)
fn first<I, T>(iter: I) -> Option<T>
where
I: Iterator<Item = T>,
{
iter.into_iter().next()
}- After (associated type only)
fn first_asso<I>(iter: I) -> Option<I::Item>
where
I: Iterator,
{
iter.into_iter().next()
}// std
let mut asso_test = 1..=10;
while let Some(item) = asso_test.next() {
println!("{:?}", item);
}
// first_asso() consumes the iterator because it calls into_iter() which takes
// ownership:
// Use the iterator directly, or use by_ref() to iterate mutably:
// 반복기를 직접 사용하거나 by_ref()를 사용하여 반복을 반복합니다:
let mut asso_test = 1..=10;
while let Some(item) = first_asso(asso_test.by_ref()) {
println!("{:?}", item);
}
- 다양한 버젼
// After (associated type only)
fn first_asso<I>(iter: I) -> Option<I::Item>
where
I: Iterator,
{
iter.into_iter().next()
}
fn first_asso_ver1<I>(iter: I) -> Option<I::Item>
where
I: Iterator<Item = i32>, // Constrain Item to be i32
{
iter.into_iter().next()
}
// Option 2: Bound the associated type by a trait
fn first_asso_ver2<I>(iter: I) -> Option<I::Item>
where
I: Iterator,
I::Item: Debug, // I::Item must implement Debug
{
iter.into_iter().next()
}
// Option 3: Use a separate type parameter with the associated type
fn first_asso_ver3<I, T>(iter: I) -> Option<T>
where
I: Iterator<Item = T>, // Connect I::Item to T
T: Debug, // Then bound T
{
iter.into_iter().next()
}- Why this is better
Twas not independently meaningful- Item already belongs to
I - Less generic noise, same power
- 이것이 더 나은 이유
T는 독립적으로 의미가 없었습니다- 항목이 이미
I에 속합니다 - 덜 일반적인 소음, 동일한 전력
2.2. sum using reduce (your original topic)|🔝|
- Before (
I,T)
use std::ops::Add;
fn sum<I, T>(iter: I) -> Option<T>
where
I: Iterator<Item = T>,
T: Add<Output = T>,
{
iter.reduce(|a, b| a + b)
}- After (associated type)
use std::ops::Add;
fn sum<I>(iter: I) -> Option<I::Item>
where
I: Iterator,
I::Item: Add<Output = I::Item>,
{
iter.reduce(|a, b| a + b)
}- Key insight
- The behavior (
Add) belongs to the item, not the iterator.
- The behavior (
- 주요 인사이트
- 동작(‘추가’)은 반복자가 아닌 항목에 속합니다.
2.3. Debug-printing items|🔝|
- Before
fn debug_items<I, T>(iter: I)
where
I: Iterator<Item = T>,
T: std::fmt::Debug,
{
for item in iter {
println!("{:?}", item);
}
}- After
fn debug_items<I>(iter: I)
where
I: Iterator,
I::Item: std::fmt::Debug,
{
for item in iter {
println!("{:?}", item);
}
}- This is the canonical Rust style.
- 이것은 표준 러스트 스타일입니다.
3.0 Here’s the non-panicking version of your sum that returns Option<I::Item> instead of panicking on an empty iterator|🔝|
3.1 Non-panicking sum using Option|🔝|
use std::ops::Add;
fn sum<I>(iter: I) -> Option<I::Item>
where
I: Iterator,
I::Item: Add<Output = I::Item>,
{
iter.reduce(|a, b| a + b)
}- Why this works
Iterator::reducealready returnsOption<Item>Some(total)if at least one element existsNoneif the iterator is empty- No
Default - No panic
- 이것이 작동하는 이유
반복::reduce는 이미Option<Item>을 반환합니다- 적어도 하나의 원소가 존재하는 경우 ‘일부(전체)’
- 반복자가 비어 있으면 ‘없음’
Dafault없음- 당황하지 마세요(No Panic)
3.2 Example 1: Non-empty Vec<i32>|🔝|
fn main() {
let v = vec![1, 2, 3];
let result = sum(v.into_iter());
println!("{:?}", result); // Some(6)
}3.3 Example 2: Empty iterator|🔝|
fn main() {
let v: Vec<i32> = vec![];
let result = sum(v.into_iter());
println!("{:?}", result); // None
}- ✅ No panic
3.4 Example 3: Handling the result safely|🔝|
fn main() {
let v = vec![10, 20, 30];
match sum(v.into_iter()) {
Some(total) => println!("sum = {}", total),
None => println!("empty iterator"),
}
}
- ✅ No panic
3.5 Example 4: String concatenation|🔝|
let words = vec![
"Rust",
" ",
"Lang"
];
let result: Option<String> = if words
.is_empty() {
None
} else {
Some(words.concat())
};
println!("{:?}", result); // Some("Rust Lang")3.6 Example 5: Custom type|🔝|
use std::ops::Add;
#[derive(Debug, Copy, Clone)]
struct MyNum(i32);
impl Add for MyNum {
type Output = MyNum;
fn add(self, rhs: MyNum) -> MyNum {
MyNum(self.0 + rhs.0)
}
}
fn main() {
let nums = vec![MyNum(1), MyNum(2), MyNum(3)];
let result = sum(nums.into_iter());
println!("{:?}", result); // Some(MyNum(6))
}3.7 Comparison summary|🔝|
| Version | Empty iterator | Trait bounds |
|---|---|---|
reduce().expect() | ❌ panic | Add |
Option<I::Item> | ✅ safe | Add |
fold + Default | ✅ safe | Add + Default |
3.8 When to use this version|🔝|
- Library / public APIs
- Functional-style code
- When “no data” is a valid outcome
- When you want zero panics
- 이 버전 사용 시기
- 라이브러리/공공 API
- 기능형 코드
- “데이터 없음”이 유효한 결과일 때
- 제로 패닉을 원할 때
같이 보면 좋은 자료들.
easy rust 시리즈