비욘드 JS: 러스트 - 패턴 매칭

dante Yoon·2023년 1월 8일
1

beyond js

목록 보기
20/20
post-thumbnail

글을 시작하며

안녕하세요, 단테입니다. 오늘은 러스트의 패턴 매칭에 대해 알아보겠습니다.
Paterns는 러스트에서 제공하는 특별한 문법입니다. if else를 사용하지 않고도 분기 처리를 할 수 있는 문법인데요, 아래 타입들의 조합에 대해 패턴 매칭을 적용할 수 있습니다.

  • Literals
  • Destructed arrays, enums, structs, or tuples
  • Variables
  • Wildcards
  • Placeholders

All the Places Patterns Can Be Used

match Arms

앞서서 계속 봤었던 패턴 매칭 문법의 구성 요소를 뜯어 볼텐데요, match 키워드와 중괄호인 arms를 이용해 아래와 같이 코드를 구성합니다.

match VALUE {
    PATTERN => EXPRESSION,
    PATTERN => EXPRESSION,
    PATTERN => EXPRESSION,
}

Option<i32> 타입의 값을 대상으로 match 키워드를 사용하는 문법을 살펴보겠습니다.

match x {
  None => None,
  Some(i) => Some(i+1),
}

match 표현식을 사용함에 있어서 필요한 것은 exhaustive 상태를 항상 만족해야 한다는 것입니다. match arms에 있는 각 경우의 수는 match expression의 대상이 되는 모든 타입을 커버할 수 있어야 합니다. 만약 타입이 0 , 1, 2, 3, 4, 5 총 6까지로 구성되어있는 타입은 match arms에서 6까지의 모든 타입을 커버할 수 있어야 합니다.

if let expression

if let 표현식은 match 문과 동일한 기능을 제공하지만 오직 한 가지의 조건을 대상으로 분기문을 실행할 떄 사용합니다. if let 은 else 문을 선택적으로 가질 수 있습니다.

fn main() {
    let favorite_color: Option<&str> = None;
    let is_tuesday = false;
    let age: Result<u8, _> = "34".parse();

    if let Some(color) = favorite_color {
        println!("Using your favorite color, {color}, as the background");
    } else if is_tuesday {
        println!("Tuesday is green day!");
    } else if let Ok(age) = age {
        if age > 30 {
            println!("Using purple as the background color");
        } else {
            println!("Using orange as the background color");
        }
    } else {
        println!("Using blue as the background color");
    }
}

if let 표현식의 단점은 컴파일러가 exhautive하게 모든 타입을 평가하지는 않는다는 것입니다

while let 조건문

if let과 유사하게 while let 조건문 루프는 패턴 매칭을 만족할 때에 한해서 반복문을 반복할 수 있게 합니다.

fn main() {
    let mut stack = Vec::new();

    stack.push(1);
    stack.push(2);
    stack.push(3);

    while let Some(top) = stack.pop() {
        println!("{}", top);
    }
}

위 코드를 실행하면 pop 메소드는 벡터의 마지막 엘리먼트를 가지고 Some(value) 타입을 반환합니다. 만약 벡터가 비게 되면 pop은 None 타입을 반환하게 되고 while 루프는 pop이 Some variant를 반환하는 한 계속 실행됩니다.

for loop

fn main() {
    let v = vec!['a', 'b', 'c'];

    for (index, value) in v.iter().enumerate() {
        println!("{} is at index {}", value, index);
    }
}

for x in y문에서는 x가 패턴으로 작동합니다. 위 예제코드에서 for loop에서 구조분해를 이용해 index, value를 사용하고 있습니다.

$ cargo run
   Compiling patterns v0.1.0 (file:///projects/patterns)
    Finished dev [unoptimized + debuginfo] target(s) in 0.52s
     Running `target/debug/patterns`
a is at index 0
b is at index 1
c is at index 2

Function Parameters

함수 파라메터도 패턴입니다.
아래 코드에서 함수 foo는 i32타입의 x 파라메터를 가지고 있습니다.

fn foo(x: i32) {
    // code goes here
}

fn main() {}

여기서 패턴은 파라메터인 x입니다. 패턴이라는 이야기는 다음 처럼 구조분해를 사용할 수 있따는 뜻입니다.

fn print_coordinates(&(x, y): &(i32, i32)) {
    println!("Current location: ({}, {})", x, y);
}

fn main() {
    let point = (3, 5);
    print_coordinates(&point); // Current location: (3, 5)
}

Refutability

패턴은 두 가지 형태로 존재할 수 있습니다. refutable(반박), irrefutable(반박 불가).
전달된 모든 가능한 값과 매칭되는 패턴은 irrefutable 합니다. let x = 5; statement에서 x는 모든 것과 매칭될 수 있기에 irrefutable 합니다. 어떤 값과 매칭되는 것이 실패할 수 있는 것은 refutable합니다. 표현식에서 Some(x)if let Some(x) = a_value와 같이 사용될 수 있는데 a_value는 None이나 Some일 수 있기 때문에 Some(x) 패턴은 매칭될 수 없습니다.

함수 파라매터와 let statement, 그리고 for loop는 irrefutable 패턴만 받아들일 수 있습니다. if let과 while let 표현식은 refutable과 irrefutable 패턴을 받아들일 수 있습니다. 하지만 컴파일러는 irrefutable 패턴에 대해 경고문구를 발생시킵니다.

irrefutable, refutable 패턴을 언제 사용할 수 있는지 살펴보겠습니다.
let statement에서 Some(x)은 refutable인데 예상대로 컴파일 되지 않습니다.

fn main() {
    let some_option_value: Option<i32> = None;
    let Some(x) = some_option_value;
}

some_option_value는 None 값인 경우 Some(x)와 매칭되지 않기 때문에 이 패턴은 refutable합니다. 하지만 let statement는 irrefutable 패턴만 받아들일 수 있습니다. let에 바인딩할 수 있는 모든 패턴을 Some(x)로 받아들일 수 없기 때문에 러스트는 컴파일러 에러를 발생시킵니다.

$ cargo run
   Compiling patterns v0.1.0 (file:///projects/patterns)
error[E0005]: refutable pattern in local binding: `None` not covered
   --> src/main.rs:3:9
    |
3   |     let Some(x) = some_option_value;
    |         ^^^^^^^ pattern `None` not covered
    |
    = note: `let` bindings require an "irrefutable pattern", like a `struct` or an `enum` with only one variant
    = note: for more information, visit https://doc.rust-lang.org/book/ch18-02-refutability.html
note: `Option<i32>` defined here
    = note: the matched value is of type `Option<i32>`
help: you might want to use `if let` to ignore the variant that isn't matched
    |
3   |     let x = if let Some(x) = some_option_value { x } else { todo!() };
    |     ++++++++++                                 ++++++++++++++++++++++

For more information about this error, try `rustc --explain E0005`.
error: could not compile `patterns` due to previous error

irrefutable 타입이 필요한 것에 refutable 패턴을 사용해야 한다면 패턴을 사용하는 코드로 변경해야 합니다. let 대신에 if let을 사용하면 됩니다. 이 패턴이 매칭되지 않을 경우 코드는 중괄호를 벗어나기 때문에 코드는 정상적으로 작동할 수 있습니다.

fn main() {
    let some_option_value: Option<i32> = None;
    if let Some(x) = some_option_value {
        println!("{}", x);
    }
}

반대로 러스트는 아래 코드에 대해서는 경고문구를 발생시키는데 언제나 매칭되는 상황에서 if let을 사용한다면 refutable 패턴을 사용했기 때문에 irrefutable 타입을 사용하라고 경고 문구가 발생합니다. if let을 사용해야할 이유가 없기 때문입니다.

if let x = 5 {
	println!("{}", x);
};
$ cargo run
   Compiling patterns v0.1.0 (file:///projects/patterns)
warning: irrefutable `if let` pattern
 --> src/main.rs:2:8
  |
2 |     if let x = 5 {
  |        ^^^^^^^^^
  |
  = note: `#[warn(irrefutable_let_patterns)]` on by default
  = note: this pattern will always match, so the `if let` is useless
  = help: consider replacing the `if let` with a `let`

warning: `patterns` (bin "patterns") generated 1 warning
    Finished dev [unoptimized + debuginfo] target(s) in 0.39s
     Running `target/debug/patterns`
5

match arm은 항상 마지막 arm을 제외하고는 refutable한 패턴을 사용해야 합니다. 마지막 arm은 irrefutable한 패턴을 사용합니다. 러스트는 match 문에서 마지막 arm만 irrefutable 패턴을 허용합니다.

패턴 문법들

유효한 패턴 매칭 문법들에 대해 좀 더 익숙해지기 위해 패턴 매칭의 대상이되는 타입에 따라 각 예제 코드들을 작성해보겠습니다.

literal matching

let x = 1; 

match x {
  1 => println!("one"),
  2 => println!("two"),
  3 => println!("three"),
  4 => println!("anything"),
}

위 예제는 특정 구체적인 값에 대한 패턴매칭을 수행하는 코드입니다.
value x 가 1이기 때문에 one을 콘솔에 출력합니다.

Matching Named variables

named variables는 irrefutable한 패턴이므로 모든 유형의 값에 대해 패턴 매칭을 적용할 수 있습니다.

match 는 새로운 스코프를 생성하고 qustnsms match 구조체 내부에 하나의 패턴으로 선언되어 구조체 외부 스코프에 있는 동일한 이름에 변수를 가리게 됩니다(shadow).

아래 예제에서 변수 x에 Some(5), y에 10을 선언했습니다. x에 대한 match 표현식을 생성했습니다. arm 내부에 있는 패턴을 살펴보고 println! 매크로를 마지막에 작성했습니다.

fn main() {
    let x = Some(5);
    let y = 10;

    match x {
        Some(50) => println!("Got 50"),
        Some(y) => println!("Matched, y = {y}"),
        _ => println!("Default case, x = {:?}", x),
    }

    println!("at the end: x = {:?}, y = {y}", x);
}

match 표현식이 실행될 때 실제 프로그램이 어떤 순서로 진행될지 코드를 따라가보겠습니다. 이 패턴의 첫번째 arm은 x랑 매칭되지 않아 다음 arm으로 넘어갑니다.

두번째 arm은 y라는 변수를 새롭게 사용합니다. 이 y는 Some이 가지고 있는 값에 바인딩됩니다. match 표현식 내부라는 새로운 스코프에서 y를 사용하기 때문에 이 새로운 y 바인딩은 Some 내부의 값에 바인딩되지 match의 외부스코프에 있는 let y = 10의 패턴 바인딩의 y 값을 사용하지 않습니다 따라서 Matched, y = 5가 출력됩니다.

만약 x가 Some(5) 대신에 None 값을 가지고 있으면 첫번째와 두번째의 arm은 건너뛰고 마지막 arm에 해당하는 _에 매칭되게 됩니다. _ arm에서 새로운 x 변수를 사용하지 않았기 때문에 이 표현식에서 x는 아무런 새로운 값에 shadow 되지 않은 외부 스코프의 x 변수 값을 가르킵니다.

match 표현식이 끝나고 스코프가 종료되면 내부 스코프의 y또한 종료되게 되며 마지막 출력문은 at the end: x = Some(5), y = 10가 되게 됩니다.

Multi patterns

match 표현식에서 | 연산자를 사용해 여러가지 패턴을 한가지 arm에서 사용할 수 있습니다.

fn main() {
    let x = 1;

    match x {
        1 | 2 => println!("one or two"),
        3 => println!("three"),
        _ => println!("anything"),
    }
}

Matching Ranges of Values with ..=

..= 문법은 처음 보는데요, 포괄 범위를 뜻합니다. 예제 코드를 보시죠.

let x = 5;

match x {
  1..=5 => println!("one through five"),
  _ => println!("something else"),
}

x가 1,2,3,4,5 중 하나의 값을 가질 때 첫번째 arm이 선택됩니다. 이 문법은 범위를 표현할 때 multi pattern인 | 연산자를 사용하는 것보다 더욱 편리합니다.

컴파일러는 범위가 비어있지는 않는지 컴파일 타임에 검증할 수 있습니다. 범위가 비었는지 아닌지 확인할 수 있는 대상 타입은 char, numeric 타입 밖에 없습니다. 즉 matching range는 char, numeric 타입 대상으로만 사용할 수 있다는 뜻입니다.

let x =  'c';

match x {
	'a'..='j' => println!("early ASCII letter"),
    'k'..='z' => println!("late ASCII letter"),
    _ => println!("something else"),
}

러스트는 'c'가 첫번째 범위에 포함되어 있다고 확인할 수 있고 "early ASCII letter"를 출력할 수 있습니다.

Destructing to Break Apart Values

우리는 구조체나 열거형, 그리고 튜플에 대해 구조분해를 사용할 수 있습니다. 확인해볼까요?

Structs 구조분해

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

fn main() {
  let p = Point { x: 0, y: 7};
  
  let Point { x: a, y: b } = p;
  assert_eq!(0, a);
  assert_eq!(7, b);
}

구조분해를 이용해 a,b를 Point의 필드 x,y 과 매칭시켰습니다.

x,y를 그대로 변수로 사용할 수도 있습니다.

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

fn main() {
    let p = Point { x: 0, y: 7 };

    let Point { x, y } = p;
    assert_eq!(0, x);
    assert_eq!(7, y);
}

다음 예제코드에서 Point의 값을 세 개의 조건에 대해 arm을 작성했습니다.
y가 필드 값이 0일 때 x 축을 출력하고 x 필드 값이 0일때 y축 값을 출력합니다.

fn main() {
    let p = Point { x: 0, y: 7 };

    match p {
        Point { x, y: 0 } => println!("On the x axis at {x}"),
        Point { x: 0, y } => println!("On the y axis at {y}"),
        Point { x, y } => {
            println!("On neither axis: ({x}, {y})");
        }
    }
}
// On the y axis at 7

첫번째 arm은 y 값을 명시함으로 인해 x축에 대한 값을 출력했습니다. p에 바인딩된 Point의 필드 y의 값이 0인 경우 이 arm이 선택됩니다.

Destructuring Enums

열거형에 대해 구조분해를 사용하겠습니다.

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

fn main() {
    let msg = Message::ChangeColor(0, 160, 255);

    match msg {
        Message::Quit => {
            println!("The Quit variant has no data to destructure.");
        }
        Message::Move { x, y } => {
            println!(
                "Move in the x direction {x} and in the y direction {y}"
            );
        }
        Message::Write(text) => {
            println!("Text message: {text}");
        }
        Message::ChangeColor(r, g, b) => println!(
            "Change the color to red {r}, green {g}, and blue {b}",
        ),
    }
}

먼저 열거형 Message를 선언했습니다.

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

msg 변수에 rgb를 의미하는 튜플 타입의 값을 바인딩했습니다.

fn main() {
    let msg = Message::ChangeColor(0, 160, 255);
}

코드를 실행하면 다음과 같이 출력됩니다. Change the color to red 0, green 160, and blue 255. 첫 arm인 Quit은 아무런 정보를 가지고 있지 않습니다. 따라서 구조분해또한 할 수 없습니다. literal 값인 Message::Quit 값에 대해서만 패턴 매칭을 수행할 수 있습니다.

    match msg {
        Message::Quit => {
            println!("The Quit variant has no data to destructure.");
        }
        Message::Move { x, y } => {
            println!(
                "Move in the x direction {x} and in the y direction {y}"
            );
        }
        Message::Write(text) => {
            println!("Text message: {text}");
        }
        Message::ChangeColor(r, g, b) => println!(
            "Change the color to red {r}, green {g}, and blue {b}",
        ),
    }

나머지 Move, Write, ChangeColor에 해당하는 필드들은 구조분해를 통해 출력하고 있음을 알 수 있습니다.

Destructuring Nested Structs and Enums

다음 코드는 ChangeColor variant가 가지고 있는 중첩 구조의 열거형에 대해서도 구조분해가 작동하는 것을 알려줍니다.

enum Color {
    Rgb(i32, i32, i32),
    Hsv(i32, i32, i32),
}

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(Color),
}

fn main() {
    let msg = Message::ChangeColor(Color::Hsv(0, 160, 255));

    match msg {
        Message::ChangeColor(Color::Rgb(r, g, b)) => {
            println!("Change color to red {r}, green {g}, and blue {b}");
        }
        Message::ChangeColor(Color::Hsv(h, s, v)) => println!(
            "Change color to hue {h}, saturation {s}, value {v}"
        ),
        _ => (),
    }
}

Destructuring Structs and Tuples

튜플과 구조체를 한데 묶어서 사용하는 변수에 대해서도 구조분해를 수행할 수 있습니다.

아래 코드에서 튜플은 또 다른 튜플 및 Point를 구조체를 엘리먼트로 가지고 있습니다.

fn main() {
    struct Point {
        x: i32,
        y: i32,
    }

    let ((feet, inches), Point { x, y }) = ((3, 10), Point { x: 3, y: -10 });
}

Ignoring Values in a pattern

match문은 항상 exhautive하게 동작해야 하기 떄문에 고려하지 않고 싶은 패턴에 대해서도 항상 처리를 해줘야 합니다. 이 경우 해당 패턴을 무시할 수 있는 _ 이나 ..을 사용해 나머지 패턴에 대한 처리를 해줄 수 있습니다.

_

아래와 같이 패턴 매칭이 아닌 함수 파라메터의 경우에도 언더스코어 _를 사용해 무시하고 싶은 파라메터를 표기할 수 있습니다.

fn foo(_: i32, y: i32) {
    println!("This code only uses the y parameter: {}", y);
}

fn main() {
    foo(3, 4);
}

패턴 매칭에서도 사용해보겠습니다. 특정 파트에 해당하는 값에 대해서도 이 규칙을 사용할 수 있습니다.

fn main() {
    let mut setting_value = Some(5);
    let new_setting_value = Some(10);

    match (setting_value, new_setting_value) {
        (Some(_), Some(_)) => {
            println!("Can't overwrite an existing customized value");
        }
        _ => {
            setting_value = new_setting_value;
        }
    }

    println!("setting is {:?}", setting_value);
}

만약 settingvalue나 new_setting_value가 None 값을 가질 때 패턴을 사용해 두번쨰 arm에 대한 로직을 작성할 수 있습니다.

아래 예제는 구조분해에서 특정 값만 가져오고 싶을 때 언더스코어 문법을 사용하는 경우를 나타냅니다.

    let numbers = (2, 4, 8, 16, 32);

    match numbers {
        (first, _, third, _, fifth) => {
            println!("Some numbers: {first}, {third}, {fifth}")
        }
    }

Ignoring Remaing Parts of a Value with ..

아래 코드에 사용된 .. 문법을 통해 특정 부분만 구조분해를 통해 사용하고 나머지 값은 무시할 수 있습니다. 아래 코드에서 y,z필드는 .. 구문을 통해 무시됩니다.

fn main() {
    struct Point {
        x: i32,
        y: i32,
        z: i32,
    }

    let origin = Point { x: 0, y: 0, z: 0 };

    match origin {
        Point { x, .. } => println!("x is {}", x),
    }
}

아래는 또 다른 예시를 보여줍니다.

fn main() {
    let numbers = (2, 4, 8, 16, 32);

    match numbers {
        (first, .., last) => {
            println!("Some numbers: {first}, {last}");
        }
    }
}

하지만 아래와 같이 ..를 여러 부분으로 나누어 사용하면 러스트 컴파일러는 이런 모호한 구문에 대한 에러를 발생시킵니다.

fn main() {
    let numbers = (2, 4, 8, 16, 32);

    match numbers {
        (.., second, ..) => {
            println!("Some numbers: {}", second)
        },
    }
}
$ cargo run
   Compiling patterns v0.1.0 (file:///projects/patterns)
error: `..` can only be used once per tuple pattern
 --> src/main.rs:5:22
  |
5 |         (.., second, ..) => {
  |          --          ^^ can only be used once per tuple pattern
  |          |
  |          previously used here

error: could not compile `patterns` due to previous error

Match Guards

match guards는 arm에 추가적인 if 조건문을 작성하는 것입니다. 아래 코드에서 각 arm에 Some(x)에 더해 x가 짝수인 경우에 첫번째 arm을 실행시키게 하고 있습니다.

fn main() {
    let num = Some(4);

    match num {
        Some(x) if x % 2 == 0 => println!("The number {} is even", x),
        Some(x) => println!("The number {} is odd", x),
        None => (),
    }
}

또 다른 예제를 보겠습니다. 아래 코드에서는 Default case, x = Some(5)가 출력됩니다.

fn main() {
    let x = Some(5);
    let y = 10;

    match x {
        Some(50) => println!("Got 50"),
        Some(n) if n == y => println!("Matched, n = {n}"),
        _ => println!("Default case, x = {:?}", x),
    }

    println!("at the end: x = {:?}, y = {y}", x);
}

아래는 or operator |와 함께 사용하는 match guard를 나타냅니다.

fn main() {
    let x = 4;
    let y = false;

    match x {
        4 | 5 | 6 if y => println!("yes"),
        _ => println!("no"),
    }
}

@ Bindings

@는 at operator라고 부릅니다. 이 연산자는 패턴매칭에 사용하는 변수가 가지고 있는 값을 가르키는데요 Message::Hello id 필드가 3..=7 범위에 포함되는지를 확인하고 싶습니다. id_variable 변수에 이 값을 바인딩하고 싶을 때 해당 arm에서 @를 사용할 수 있습니다.

fn main() {
    enum Message {
        Hello { id: i32 },
    }

    let msg = Message::Hello { id: 5 };

    match msg {
        Message::Hello {
            id: id_variable @ 3..=7,
        } => println!("Found an id in range: {}", id_variable),
        Message::Hello { id: 10..=12 } => {
            println!("Found an id in another range")
        }
        Message::Hello { id } => println!("Found some other id: {}", id),
    }
}

위 예제에서 Found an id in range: 5가 출력되었는데요 id_variable @를 범위 3..=7 이전에 선언했기 때문에 범위에 매칭될때 해당 id 필드의 값을 id_variable에 바인딩할 수 있는 것입니다.

글을 마치며

러스트의 패턴매칭은 매우 유용하여 타입스크립트를 사용하는 환경에서 라이브러리로 만들어 활용하기도 합니다.

오늘 배운 패턴매칭을 잘 눈여겨 보시고 나중에 잘 활용하시기 바랍니다.
긴 글 읽어주셔서 감사합니다.

profile
성장을 향한 작은 몸부림의 흔적들

0개의 댓글