#33 고급 기술들 上

Pt J·2020년 9월 23일
0

[完] Rust Programming

목록 보기
36/41
post-thumbnail

이 시리즈는 Rust 공식문서를 통해 공부한 흔적임을 밝힙니다.

이번 시간에는 트레이트, 자료형, 함수와 클로저에 대한 고급 기술을 살펴보도록 하겠다.

고급 트레이트 Advanced Trait

우리는 오래 전 트레이트에 대해 공부한 바가 있다.
그 땐 우리가 아직 Rust의 많은 기능을 다루지 못하는 상태였기에
트레이트에 대한 고급 기술은 설명하지 않고 넘어갔는데 이제 그 이야기를 해도 될 것 같다.

연관 자료형 Associated Type

우리가 전에 간단히 설명하고 이후에 더 자세히 알아보자고 한 것 중에
연관 자료형이라는 녀석이 있었다.
연관 자료형이란 트레이트를 구현할 때 어떤 자료형을 사용하여 구현하든 상관 없지만
얘네들은 다 같은 자료형이어야 한다, 하고 적어두는 자료형 자리지정자를 의미한다.
type이라는 키워드와 함께 사용되며
트레이트를 구현할 때 이 연관 자료형이 실제로는 어떤 구체적인 자료형으로 구현되는지 명시해야 한다.
이로써 트레이트를 작성할 땐
그것을 구현하는 사람이 어떤 자료형을 사용할지는 신경쓰지 않고 작성할 수 있다.

예를 들어, 우리는 트레이트에 대해 다룰 때
Iterator 트레이트는 다음과 같이 선언되어 있다는 것을 배웠다.

pub trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;

    // methods with default implementations elided
}

여기서 type Item;이 연관 자료형을 선언하는 부분이다.

그리고 이것을 구현할 땐 다음과 같이 작성할 수 있는 것을 배웠다.

// snip
impl Iterator for Counter {
    type Item = u32;

    // snip
}
// snip

만약 우리가 "임의의 자료형이면 제네릭을 써도 되는 거 아냐?" 라며 제네릭을 사용했다면
우리는 next 메서드를 호출할 때마다 그 자료형을 명시해야 했을 것이다.
하지만 연관 자료형은 트레이트를 구현할 때 구체적인 자료형이 정해지기에
자료형을 따로 명시해주지 않고 next만으로 호출할 수 있다.

기본 제네릭 자료형 매개변수 Default Generic Type Parameter

우리는 제네릭 자료형을 사용할 때 <자리지정자자료형>와 같은 표기를 한다.
자리지정자 자료형은 주로 T와 같은 짧은 이름으로 짓고 말이다.
제네릭을 배울 때 따로 언급하지 않았지만 우리는
제네릭의 실제 자료형을 명시해주지 않았을 경우에 default 자료형을 사용하도록 할 수 있다.
이를 위해 필요한 게 기본 제네릭 자료형 매개변수다.
이것은 <자리지정자자료형=실제자료형> 형태로 사용된다.
기본 제네릭 자료형 매개변수는 연산자 오버로딩에 유용하게 사용된다.

Rust는 기본적으로 사용자 정의 연산자를 지원하지 않지만
std::ops 모듈의 연산자와 관련된 트레이트를 구현하면 연산자를 오버로딩 할 수 있다.

예를 들어, 다음과 같은 코드의 작성이 가능하다.
이것은 두 구조체 변수의 덧셈을 지원하기 위해 Add 트레이트를 구현한 것이다.

peter@hp-laptop:~/rust-practice/chapter19$ cargo new overload_add
     Created binary (application) `overload_add` package
peter@hp-laptop:~/rust-practice/chapter19$ cd overload_add/
peter@hp-laptop:~/rust-practice/chapter19/overload_add$ vi src/main.rs

src/main.rs

use std::ops::Add;

#[derive(Debug, PartialEq)]
struct Point {
    x: i32,
    y: i32,
}

impl Add for Point {
    type Output = Point;

    fn add(self, other: Point) -> Point {
        Point {
            x: self.x + other.x,
            y: self.y + other.y,
        }
    }
}

fn main() {
    assert_eq!(
        Point { x: 1, y: 0 } + Point { x: 2, y: 3 },
        Point { x: 3, y: 3 }
    );
}
peter@hp-laptop:~/rust-practice/chapter19/overload_add$ cargo run
   Compiling overload_add v0.1.0 (/home/peter/rust-practice/chapter19/overload_add)
    Finished dev [unoptimized + debuginfo] target(s) in 0.24s
     Running `target/debug/overload_add`
peter@hp-laptop:~/rust-practice/chapter19/overload_add$ 

여기서, 우리가 구현한 Add 트레이트는 다음과 같이 선언되어 있다.

trait Add<Rhs=Self> {
    type Output;

    fn add(self, rhs: Rhs) -> Self::Output;
}

Rhs라는 제네릭 자료형을 사용하고 있으며,
따로 명시해주지 않을 경우 이것을 구현한 자료형이 자동으로 들어간다.
우리는 Point 구조체에서 이 트레이트를 구현했으며 덧셈의 대상 또한 Point 인스턴스이므로
default 자료형을 그대로 사용할 수 있다.

반면, 덧셈의 대상이 다른 자료형일 경우 트레이트를 구현할 때 이를 명시해주어야 한다.
예를 들어 다음과 같은 경우가 그렇다.

peter@hp-laptop:~/rust-practice/chapter19/overload_add$ cd ..
peter@hp-laptop:~/rust-practice/chapter19$ cargo new overload_add_2 --lib
     Created library `overload_add_2` package
peter@hp-laptop:~/rust-practice/chapter19$ cd overload_add_2/
peter@hp-laptop:~/rust-practice/chapter19/overload_add_2$ vi src/lib.rs

src/lib.rs

struct Millimeters(u32);
struct Meters(u32);

impl Add<Meters> for Millimeters {
    type Output = Millimeters;

    fn add(self, other: Meters) -> Millimeters {
        Millimeters(self.0 + other.0 * 1000)
    }
}

기본 제네릭 자료형 매개변수는
기존 코드와의 호환성을 유지하면서 자료형을 확장할 때
기존 코드의 자료형을 default로 두고 다른 자료형은 제네릭으로 명시하게 하는 용도로 사용되거나
대부분의 사용자가 사용하는 방식이 있고 특별한 경우에 커스터마이징이 필요할 때
대부분이 사용하는 방식을 default로 두고 특별한 경우 제네릭으로 명시하게 하는 용도로 사용된다.
위의 Add 예제의 경우 후자에 해당한다.

같은 이름의 메서드

여러 개의 트레이트를 구현하게 될 경우
때로는 서로 다른 트레이트가 같은 이름의 메서드를 사용하는 것을 마주칠 때가 있다.
구현할 땐 각자 구현했는데 호출하려고 하니
이 녀석이 구현하고 있는 두 개의 트레이트 중 어느 쪽의 메서드인지 알 수 없는 문제가 생긴다.
심지어 트레이트 메서드가 아닌 자료형 자체 메서드 중에도 그 이름이 있다면?

자료형 자체 메서드와 트레이트 메서드의 이름이 겹칠 경우에는
전자가 호출되도록 되어 있다.

예를 들어, 다음과 같이 작성할 경우 Human의 메서드를 호출한다.

peter@hp-laptop:~/rust-practice/chapter19/overload_add_2$ cd ..
peter@hp-laptop:~/rust-practice/chapter19$ cargo new fly_methods
     Created binary (application) `fly_methods` package
peter@hp-laptop:~/rust-practice/chapter19$ cd fly_methods/
peter@hp-laptop:~/rust-practice/chapter19/fly_methods$ vi src/main.rs

src/main.rs

trait Pilot {
    fn fly(&self);
}

trait Wizard {
    fn fly(&self);
}

struct Human;

impl Pilot for Human {
    fn fly(&self) {
        println!("This is your catain speaking.");
    }
}

impl Wizard for Human {
    fn fly(&self) {
        println!("Up!");
    }
}

impl Human {
    fn fly(&self) {
        println!("*waving arms furiously*");
    }
}

fn main() {
    let person = Human;
    person.fly();
}
peter@hp-laptop:~/rust-practice/chapter19/fly_methods$ cargo run
   Compiling fly_methods v0.1.0 (/home/peter/rust-practice/chapter19/fly_methods)
    Finished dev [unoptimized + debuginfo] target(s) in 0.18s
     Running `target/debug/fly_methods`
*waving arms furiously*
peter@hp-laptop:~/rust-practice/chapter19/fly_methods$ 

그런데 만약 트레이트의 메서드를 호출하고 싶다면 어떻게 해야 할까?

우리는 메서드 앞에 트레이트 이름을 명시하고 그 인자로 구조체 인자를 전달함으로써
어떤 메서드를 호출하는지 명확히 할 수 있다.

peter@hp-laptop:~/rust-practice/chapter19/fly_methods$ vi src/main.rs

src/main.rs

// snip

fn main() {
    let person = Human;
    Pilot::fly(&person);
    Wizard::fly(&person);
    person.fly();
}
peter@hp-laptop:~/rust-practice/chapter19/fly_methods$ cargo run
   Compiling fly_methods v0.1.0 (/home/peter/rust-practice/chapter19/fly_methods)
    Finished dev [unoptimized + debuginfo] target(s) in 0.18s
     Running `target/debug/fly_methods`
This is your catain speaking.
Up!
*waving arms furiously*
peter@hp-laptop:~/rust-practice/chapter19/fly_methods$ 

물론 Human의 메서드를 호출할 때도 Human::fly(&person);으로 호출할 수도 있지만
어떤 메서드인지 명백한 상황에서는 굳이 이렇게 작성할 필요는 없다.

연관함수의 경우에는...

그런데 만약 구현할 트레이트의 연관함수와 구조체 자체 연관함수가 이름이 같다면 어떨까?
연관함수의 경우 self를 인자로 가지고 있지 않아 누가 호출하는지 명시해줄 수 없다.

일단 이 녀석도 따로 명시해주지 않으면 구조체 자체 연관함수가 호출된다는 것을 확인할 수 있다.

peter@hp-laptop:~/rust-practice/chapter19/fly_methods$ cd ..
peter@hp-laptop:~/rust-practice/chapter19$ cargo new assoc_func
     Created binary (application) `assoc_func` package
peter@hp-laptop:~/rust-practice/chapter19$ cd assoc_func/
peter@hp-laptop:~/rust-practice/chapter19/assoc_func$ vi src/main.rs

src/main.rs

trait Animal {
    fn baby_name() -> String;
}

struct Dog;

impl Dog {
    fn baby_name() -> String {
        String::from("Spot")
    }
}

impl Animal for Dog {
    fn baby_name() -> String {
        String::from("puppy")
    }
}

fn main() {
    println!("A baby dog is called a {}", Dog::baby_name());
}
peter@hp-laptop:~/rust-practice/chapter19/assoc_func$ cargo run
   Compiling assoc_func v0.1.0 (/home/peter/rust-practice/chapter19/assoc_func)
    Finished dev [unoptimized + debuginfo] target(s) in 0.26s
     Running `target/debug/assoc_func`
A baby dog is called a Spot
peter@hp-laptop:~/rust-practice/chapter19/assoc_func$ 

그런데 만약 우리가 원했던 게 Animal의 메서드였다면 어떻게 해야 할까?
메서드에서처럼 Animal::baby_name();이라고 하기엔 누가 호출하는지 알 수가 없다.

따라서 이런 경우에는 DogAnimal처럼 사용하겠다고 명시하여 호출해야 한다.
<자료형 as 트레이트>::연관함수와 같은 형태로 작성할 수 있다.
우리는 그런 형태를 정규화된 구문fully qualified syntax이라고 한다.

peter@hp-laptop:~/rust-practice/chapter19/assoc_func$ vi src/main.rs

src/main.rs

// snip

fn main() {
    println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
}
peter@hp-laptop:~/rust-practice/chapter19/assoc_func$ cargo run
   Compiling assoc_func v0.1.0 (/home/peter/rust-practice/chapter19/assoc_func)

# snip warning

    Finished dev [unoptimized + debuginfo] target(s) in 0.28s
     Running `target/debug/assoc_func`
A baby dog is called a puppy
peter@hp-laptop:~/rust-practice/chapter19/assoc_func$ 

수퍼 트레이트 super trait

경우에 따라 어떤 트레이트에서 다른 트레이트의 기능을 사용해야 하는 경우도 있다.
이 때 그 의존성을 가진 '다른 트레이트'도 구현해주어야 한다.
우리는 실제 구현하고자 하는 트레이트를 의존성 트레이트의 수퍼 트레이트라고 칭한다.

예를 들어, 우리가 어떤 값을 출력할 때 *로 테두리를 만들어 꾸며주는 트레이트를 만든다고 하자.
이것은 Outline_Print 트레이트에 outline_print라는 이름의 메드로 선언될 것이다.
그런데 우리가 이 녀석을 구현할 때 Display 트레이트의 기능을 이용하고자 한다면
우리의 Outline_Print 트레이트가 Display 트레이트의 수퍼 트레이트가 된다.
이로서 우리는 Outline_Print 트레이트는 Display 트레이트가 구현된 경우에만
추가적으로 같이 구현할 수 있다는 것을 나타낼 수 있다.
우리는 트레이트를 선언할 때 trait 수퍼트레이트: 의존성트레이트와 같이 선언함으로써
이 수퍼 트레이트의 의존성 트레이트를 명시할 수 있다.

peter@hp-laptop:~/rust-practice/chapter19/assoc_func$ cd ..
peter@hp-laptop:~/rust-practice/chapter19$ cargo new outline_print
     Created binary (application) `outline_print` package
peter@hp-laptop:~/rust-practice/chapter19$ cd outline_print/
peter@hp-laptop:~/rust-practice/chapter19/outline_print$ vi src/main.rs

src/main.rs

use std::fmt;

trait OutlinePrint: fmt::Display {
    fn outline_print(&self) {
        let output = self.to_string();
        let len = output.len();
        println!("{}", "*".repeat(len + 4));
        println!("*{}*", " ".repeat(len + 2));
        println!("* {} *", output);
        println!("*{}*", " ".repeat(len + 2));
        println!("{}", "*".repeat(len + 4));
    }
}

struct Point {
    x: i32,
    y: i32,
}

impl OutlinePrint for Point {}
impl fmt::Display for Point {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "({}, {})", self.x, self.y)
    }
}

fn main() {
    let p = Point {
        x: 1,
        y: 3,
    };
    p.outline_print();
}
peter@hp-laptop:~/rust-practice/chapter19/outline_print$ cargo run
   Compiling outline_print v0.1.0 (/home/peter/rust-practice/chapter19/outline_print)
    Finished dev [unoptimized + debuginfo] target(s) in 0.28s
     Running `target/debug/outline_print`
**********
*        *
* (1, 3) *
*        *
**********
peter@hp-laptop:~/rust-practice/chapter19/outline_print$ 

만약 위 코드에서 Display 트레이트를 구현하는 부분을 생략한다면
그것이 구현되지 않아 사용할 수 없다는 에러가 발생하며 컴파일을 거부할 것이다.

newtype 패턴

우리는 트레이트에 대해 배우면서
트레이트 또는 자료형 둘 중 하나는 로컬에 있어야 그것을 구현할 수 있다고 했다.
그런데 이걸 우회하는 방법이 있긴 하다.
newtype 패턴을 이용하면 외부 자료형에 외부 트레이트를 구현할 수 있다.

newtype 패턴은 하나의 필드를 갖는 튜플 구조체에 새로운 자료형을 생성하는 방식이다.
이 하나의 필드에 우리가 사용하려는 외부 자료형을 넣고
우리의 튜플 구조체는 일종의 wrapper로서 작용하도록 한다.
그리고 이 wrapper 자료형은 로컬 자료형이므로 이제 여기에 외부 트레이트를 구현할 수 있다.
외부 자료형과 외부 트레이트를 사용하여 새 로컬 자료형을 생성하는 것이다.

예를 들어, wrapper로 감싼 Vec<String>Display를 구현하는 예제를 살펴보자.

peter@hp-laptop:~/rust-practice/chapter19/outline_print$ cd ..
peter@hp-laptop:~/rust-practice/chapter19$ cargo new newtype_wrapper
     Created binary (application) `newtype_wrapper` package
peter@hp-laptop:~/rust-practice/chapter19$ cd newtype_wrapper/
peter@hp-laptop:~/rust-practice/chapter19/newtype_wrapper$ vi src/main.rs

src/main.rs

use std::fmt;

struct Wrapper(Vec<String>);

impl fmt::Display for Wrapper {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "[{}]", self.0.join(", "))
    }
}

fn main() {
    let w = Wrapper(vec![String::from("hello"), String::from("world")]);
    println!("w = {}", w);
}
peter@hp-laptop:~/rust-practice/chapter19/newtype_wrapper$ cargo run
   Compiling newtype_wrapper v0.1.0 (/home/peter/rust-practice/chapter19/newtype_wrapper)
    Finished dev [unoptimized + debuginfo] target(s) in 0.28s
     Running `target/debug/newtype_wrapper`
w = [hello, world]
peter@hp-laptop:~/rust-practice/chapter19/newtype_wrapper$ 

외부 자료형인 Vector에 외부 트레이트인 Display가 구현되어
벡터 안에 있는 자료들이 ,로 구분되어 출력된 것을 확인할 수 있다.

다만 이 기법의 단점은 내부 자료형의 메서드를 직접 사용할 수 없고
직접 사용하기 위해서는 그것을 외부에서 사용할 수 있도록
wrapper 자료형에 다시 구현해주어야 한다.
이것은 상당히 번거로운 작업이 될 수 있다.

내용이 많이 길어진 것 같으니 자료형, 함수와 클로저에 대한 고급 기술은 다음에 알아보도록 하겠다.

이 포스트의 내용은 공식문서의 19장 2절 Advanced Traits에 해당합니다.

profile
Peter J Online Space - since July 2020

0개의 댓글