러스트에서 패턴이 표현식과 비슷하게 생겼음에도 정반대의 동작을 하는 이유를, ‘듀얼(쌍대)’이라는 관점에서 설명한다.
URL: https://h2co3.github.io/pattern/
Rust-Users 포럼에서는 하루도 빠짐없이 누군가가 |&foo| { ... } 또는 match bar { Some(&foo) => ... } 같은 코드가 & 뒤에 오는 값을 왜 이동(move)시키는지 묻는 의식 같은 장면을 볼 수 있다. 우리 모두 &는 참조(포인터 생성)이며 절대 역참조(dereference)가 아니라고 배워 왔는데 말이다.
이 혼란의 배경은 **문법(syntax)**에 있다. 패턴과 그에 대응하는 표현식은 동일하거나 비슷한 문법을 공유한다. 여기서 **“디자인 실수!!!”**라고 외치고 싶을지도 모른다. 하지만 이런 구조들이 같은 문법을 공유하면서도(게다가) _정반대_의 동작을 하는 것은 바로 그게 올바른 선택이다. 왜 그런지 설명해 보겠다.
표현식(expression)은 값을 _구성(build)_하기 위한 언어 구성 요소다. 조금 다르게 말하면, 어떤 표현식들은 값을 다른 값으로 감싸는(wrap) 데 사용할 수 있다. 예를 들어 변수 foo나 리터럴 42 같은 “원자적(atomic)” 값이 있다고 하자:
Some(foo)와 Some(42)는 값을 Option<T> 안에 감싼 값을 만든다.&foo와 &42는 피연산자에 대한 포인터를 만든다. 즉, 한 단계의 간접 참조로 “감싼” 것이다. (이 글의 논의 목적상, 값을 컨테이너 안에 넣는 관계와 어떤 값을 간접적으로 가리키는 관계는 개념적으로 매우 유사하다.)(foo, 42)는 두 개의 개별 값을 2-튜플(즉, 쌍)로 묶는다. 두 개의 원자적 값 대신, 둘을 모두 포함하는 하나의 복합 값으로 감싼 셈이다.MyStruct { foo }는 foo라는 필드에 변수 foo의 소유권을 넘겨받는 MyStruct 구조체를 생성한다. 따라서 변수가 들고 있던 값이 이제 구조체 안에 감싸졌다.이제 우리가 이 “래퍼(wrapper)”들에서 내부 값을 꺼내고 싶다면 두 가지 방법이 있다. 첫 번째이자 가장 직관적인 방법은 감싸기의 **역(inverse)**을 수행하는 다른 표현식을 사용하는 것이다. 즉:
Some(foo).unwrap() — Option 타입에 있는 함수를 사용해 들어 있는 값을 되돌려 받을 수 있다(비어 있으면 패닉).*&foo와 *&42 — 참조나 포인터를 역참조 해서 그 “포함된”(가리키는/참조하는) 값에 도달할 수 있다. (단, 이것만으로는 Copy 타입에서만 동작한다. 참조는 항상 유효한 값을 가리켜야 하므로, 참조에서 값을 이동(move)해 나오는 것은 허용되지 않는다.)(foo, 42).0는 foo의 값을 내고, (foo, 42).1은 42의 값을 낸다. 이런 인덱스 기반 필드 접근 문법은 튜플의 특정 구성 요소에 접근하고 나머지는 무시할 수 있게 해 준다.MyStruct { foo }.foo도 마찬가지로 필드 foo의 값을 돌려준다. 어떤 의미에서는 그 필드를 둘러싼 래퍼 구조체를 “벗겨내는” 것이다.하지만 값에서 래퍼의 층을 벗겨내는 방법은 이것만이 아니다. 다른(그리고 덜 자명할 수 있는) 방법은 **패턴 매칭(pattern matching)**을 사용하는 것이다.
패턴 매칭은 (감싸진) 값이 주어졌을 때, 그 전체(감싸진) 값의 모양(shape)을 기술하는 것을 뜻한다. 그리고 우리가 관심 있는 부분에만 “이름을 붙인다”(기술 용어로는 바인딩(binding)을 만든다). 구체적으로는 다음처럼 보일 수 있다:
rustmatch Some(42) { Some(value) => println!("the value is {}", value), None => println!("no value"), }
이는 Option을 검사하는 예다. 또는 구조체를 분해할 수도 있다:
rustlet MyStruct { foo } = my_struct_value;
이후에는 바인딩 foo를 다른 변수처럼 사용할 수 있고, 그 안에는 my_struct_value.foo 필드의 값이 들어 있다. 튜플도 비슷하게 할 수 있다:
rustlet (_, forty_two) = (foo, 42);
이는 첫 번째 튜플 필드는 버리고, 두 번째 필드의 값을 변수 forty_two에 바인딩한다. 마지막으로 포인터/참조도 동일하게 동작한다:
rustlet &forty_two = &42;
이 역시 값 42를 변수 forty_two로 복사한다.
핵심은 패턴 매칭을 사용할 때:
match 표현식의 각 “갈래(arm)”에서 패턴을 사용해 값의 전체 형태를 기술한다.match의 이른바 디스크리미네이터(discriminator) 표현식은 우리가 매칭(때로는 ‘디스트럭처링(destructuring)’이라고도 부름)하려는 값 또는 _표현식_이다.내가 말하려는 요점은 패턴은 표현식이 아니다라는 것이다. 대신 패턴은 표현식에 대한 일종의 **듀얼(쌍대, dual)**이다(수학에서의 의미를 느슨하게 적용한 것으로, https://math.stackexchange.com/questions/1518509/what-does-dual-mean-exactly-in-mathematics 를 참고). 즉, 비슷하게 생겼지만 역연산을 수행한다. 혹은 같은 연산을 하되, “안쪽에서 바깥쪽으로” 수행한다고 생각해도 된다.
Rust에는 패턴이 곳곳에 존재하므로 이를 이해하는 것이 중요하다. 특히 패턴은 다음 위치에 나타날 수 있다:
let 문if let 표현식match 표현식마지막은 놀라울 수도 있다. 즉, 예를 들어 다음처럼 쓸 수 있다는 뜻이다:
rustfn function_taking_my_struct(MyStruct { foo }: MyStruct) { println!("foo = {}", foo); }
그리고 MyStruct 타입 값을 넘겨 호출한다:
rustfunction_taking_my_struct(MyStruct { foo: 1337 });
클로저도 마찬가지로 만들고 호출할 수 있다:
rustlet closure_1 = |MyStruct { foo }| { println!("foo = {}", foo); }; closure_1(MyStruct { foo: 1337 }); let closure_2 = |&forty_two| { println!("forty-two = {}", forty_two); }; closure_2(&42);
(|...| 문법이 C++ 람다의 캡처 모드 문법(환경 변수를 값/참조로 캡처할지 [대괄호]로 지정)과 비슷해 보일 수 있지만, Rust 클로저에서 |...| 부분은 캡처 리스트가 아니다. 그저 클로저의 일반적인 인자 목록일 뿐이다.)
더 나아가 패턴에는 반증 가능(refutable) 패턴과 반증 불가능(irrefutable) 패턴이라는 하위 분류가 있다. 이는 어떤 패턴을 값에 대해 무조건 바인딩할 수 있는지(성공 여부가 컴파일 타임에 완전히 결정되는 경우를 irrefutable이라 함), 아니면 디스트럭처링되는 값의 런타임 상태에 따라 조건부로만 매칭될 수 있는지(refutable)와 관련이 있다. 다만 패턴이 무엇인지 이해하는 데 이 구분은 본질적이지는 않다.