12P by xguru 2일전 | favorite | 댓글 3개

Rust의 letconst

  • let은 새로운 변수를 선언하는 데 사용됨
    • let PAT = EXPR; 형태로, 보기보단 더 강력함
    • 패턴 매칭과 결합하여 편리한 기능을 제공
      • let (a, b) = (5, 10);
      • let maybe_string: Option<String> = ..;
      • let Some(value) = maybe_string else { panic!("die horribly")};
  • const는 컴파일 타임에 계산되어 컴파일된 코드에 직접 포함되는 상수
    • const MY_VAR: &str = "heyyyyyyyy man"; const SECRET: i32 = 0x1234;
    • const IDENT: TYPE = EXPR; 형태로, 타입을 명시해야 하며 패턴을 사용할 수 없음

헷갈리게 하는 것

  • const는 선언 순서에 상관없이 사용 가능함 (hoisting)
// X가 Y 뒤에 정의되어 있어도 컴파일됨  
const Y: i32 = X + X;  
const X: i32 = 5;  
  • 함수 내부에서도 선언 가능하며, 그 상태에서도 호이스팅도 가능
fn oh_boy() -> i32 {  
	return X;  
	const X: i32 = 5;  
	// ^ 컴파일 되며 동작함. 워닝 없음!  
}  
  • 자바스크립트 출신으로 이제 막 Rust를 배우는 프로그래머와 함께 작업하는 경우, 이 기능은 그들을 당황하게 만들 수 있는 훌륭한 기능임
  • 훌륭한 기능의 무해한 결과인데, 이제 해로운 결과를 작성해보기로 함

Rust의 Match

// let PAT = EXPR;  
let x = 5;  
  
// 이 경우, `x`는 패턴임. `5`를 `x`에 넣을 수 있는지 확인함  
// 이 패턴은 항상 매치됨 -- 항상 5를 `x`라는 변수에 넣을 수 있음   
  
// 모든 패턴이 반드시 매치될 필요는 없음. 예를 들어:  
let (5, x) = (a, b);  
// 여기서 표현식은 a == 5인 경우에만 패턴과 "매치"  
//  
// 이를 "반박 가능한(refutable)" 패턴이라고 함  
//  
// `let` 선언에서, 반박 가능한 패턴은 "거부된(refused)" 경우를 처리해야 함:  
let (5, x) = (a, b) else { panic!() };  
//  
// ...그렇지 않으면 "조건부로 존재하는(conditionally existing)" 변수를 갖게 될 수 있는데, 이는 좋지 않음  
  • 그럼 match에 대해 알아봅시다. match는 무엇일까요?
// match는 패턴과 매치될 경우 수행할 작업의 목록  
//  
// match EXPR {  
//    PAT => EXPR  
//    PAT => EXPR  
//    ..  
// }  
  
match (a, b) {  
	(5, x) => {  
		// 만약 (a,b)가 (5,x)와 매치되면, 이 블록이 실행됨  
	},  
	(x, 5) => {  
		// 같은 방식으로: 만약 (a,b)가 (x, 5)와 매치되면..  
	},  
	(x, y) => {  
		// 그리고 이것은 "모든 것을 잡아내는" 패턴으로, let (x,y) = (a,b)가 동작하는 방식과 같음  
	}  
}  

고통을 줘 봅시다

  • 사람들을 혼란스럽게 하는 것도 재미있지만, 완전한 불행과 실제 버그를 야기하는 것은 어떨까?
  • 내가 보기엔 이것이 Rust의 가장 미묘한 문법임:
    • 이 글에서 가장 흥미로운 한 줄 : Rust의 가장 미묘한 문법은 상수 자체가 패턴이라는 것
  • 이 문법은 매칭 주변에 몇 가지 좋은 ergonomic을 추가함:
let input: i32 = ..;  
  
const GOOD: i32 = 1;  
const BAD: i32 = 2;  
  
match input {  
	// 이것은 input == GOOD인지 확인. 왜냐하면 GOOD은 상수이기 때문  
	GOOD => println!("input was 1"),  
	// 이것은 input == BAD인지 확인. 왜냐하면 BAD는 상수이기 때문.  
	BAD => println!("input was 2"),  
	// 이것은 otherwise = input으로 정의하고, 항상 매치됨...  
	otherwise => println!("input was {otherwise}"),  
}  

그러나 상수를 대문자로 쓰는 것은 단순히 관례일 뿐. 그렇게 하지 말라는 컴파일러 경고일 뿐임.

const good: i32 = 1;  
const bad: i32 = 2;  
match input {  
	// 음...  
	good => {},  
	bad => {},  
	otherwise => {},  
}  

이제 우리는 동일해 보이는 세 개의 분기를 가지고 있지만, 그것들이 하는 일은 해당 이름의 상수가 존재하는지에 따라 달라짐!
더 나빠져 봅시다. 아래에선 어떤 일이 일어날까?

const GOOD: i32 = 1;  
match input {  
	// 오타...  
	GOD => println!("input was 1"),  
	otherwise => println!("input was not 1")  
}  

여기서는 컴파일러 경고가 나타나겠지만, 이 코드는 항상 input was 1을 출력할 것
또는 좀 더 현실적으로:

// 이런, 실수로 이 임포트를 주석 처리하거나 삭제했음  
// use crate::{SOME_GL_CONSTANT, OTHER_THING}  
  
// 이런!  
match value {  
	SOME_GL_CONSTANT => ..,  
	OTHER_THING => ..,  
	_ => ..,  
}  

이것은 사람들을 혼란스럽게 함. 특히 그들이 열거형으로 멋진 것들을 시도할 때 더욱.

enum MyEnum {  
	A, B, C  
}  
  
// 보통은 이렇게 작성함  
match value {  
	MyEnum::A => ..,  
	MyEnum::B => ..,  
	MyEnum::C => ..,  
}  
  
// 하지만 이렇게 작성할 수도 있음  
use MyEnum::*;  
match value {  
	A => {},  
	B => {},  
	C => {}  
}  
// 그리고 나서, 만약 MyEnum을 변경한다면...  
enum MyEnum { A, B, D, E };  
use MyEnum::*;  
  
// 이것은 여전히 컴파일됨!  
match value {  
	A => {},  
	B => {},  
	C => {},  
}  
  
// `C`는 이제 "모든 것을 잡아내는" 패턴이 됨. 왜냐하면 `C`와 같은 것이 범위 내에 없기 때문.  
// 여러분은 let C = value를 하고 있는 것이고, 이는 항상 매치됨!!!  

Clippy는 이렇게 하지 말라고 경고하는 많은 규칙을 가지고 있음. 왜냐하면 이것이 사람들을 항상 혼란스럽게 하기 때문.
그러나 이것은 더욱 혼란스럽게 만들 수 있음:

// x를 5에 irrefutably 바인딩...  
let x = 5;  
  
// ...잠깐만요...  
const x: i32 = 4;  

이 코드는 컴파일되지 않음. 왜냐하면 const x는 패턴이고, 상수는 호이스팅되며, 이제 이 코드는 다음과 같이 평가되기 때문:

let 4 = 5;  
  
// error[E0005]: refutable pattern in local binding  
//  --> src/main.rs:3:5  
//   |  
// 3 | let x = 5;  
//   |     ^  
//   |     |  
//   |     패턴 `i32::MIN..=3_i32`와 `5_i32..=i32::MAX`가 커버되지 않음  
//   |     누락된 패턴은 `x`가 새로운 변수가 아닌 상수 패턴으로 해석되기 때문에 커버되지 않음  
//   |     도움말: 대신 변수를 도입하세요: `x_var`  
//   |  
//   = 참고: `let` 바인딩은 "irrefutable pattern"을 필요로 함. 예를 들어 `struct`나 하나의 variant만 가진 `enum`처럼  

"expr이 4와 같다"는 반박할 수 없는 매치가 아니며, 그렇지 않은 경우를 처리하지 않음

주위 모두를 짜증나게 만들기

// `maybe`가 Option<&str>이라고 가정. 어떤 텍스트일 수도 있고, None일 수도 있음.  
let maybe_username: Option<&str> = ..;  
  
// 이것은 한 줄 매치에서 Rust의 일반적인 패턴. 이것이 Some(..)과 매치한다면 우리는 그 문자열로 무언가를 할 수 있음.  
if let Some(username) = maybe_username {  
	// 그래서 이 코드는 username이 존재하면 실행됨...  
	return username.to_uppercase();  
}  
  
// 그런데 말이죠... 이제 그 코드는 'username'이 Some("hey")과 매치할 때만 실행됨  
const username: &str = "hey";  

상수 호이스팅과 상수가 패턴이라는 사실의 조합은 여러분이 수수께끼 같은 Rust 코드를 작성할 수 있게 해줌

이것은 실제 문제는 아님

  • 현실적으로, 이것이 혼란스러울 수 있는 유일한 이유는 여러분이 let UPPERCASEconst lowercase를 작성할 수 있다는 것
  • 만약 대문자로 시작하는 변수를 만드는 것이 lint 오류였다면, 혼란은 일어나지 않을 것
    • 열거형 variant나 상수와 매치하려고 할 때 실수로 무언가를 바인딩할 수는 없을 것이기에
  • 하지만 분명히 하자면, 이것은 단지 언어의 재미있는 특이점일 뿐
macro_rules! f {  
  ($cond: expr) => {  
    if let Some(x) = $cond {  
      println!("i am some == {x}!");  
    } else {  
      println!("i am none");  
    }  
  }  
}  
  
fn main() {  
    f!(Some(100));  
  
    {  
        f!(Some(100));  
        return;  
  
        const x: i32 = 5;  
    }  
}  

사실 큰 문제는 아닌게 웬만한 개발 환경에는 랭귀지 서버가 있고
거기서 다 추론해서 보여주기때문이죠

러스트로버 랭귀지 서버의 기반인 rust-analyzer는 꽤나 강력한도구거든요

그냥 어느 언어든 있는 다크패턴들을 모아다가
이거는 헷갈림을 유발할 수 있음!

이런 느낌의 글인거죠

헐... 스럽네요. 러스트는 이걸 어찌할 계획일까요?