rokdocs
v0.1

5.1. 변수 선언

let 키워드 (키워드) 로 새 변수를 선언합니다. 이 문서에서는 그 형태와 함께, 재할당과 새 선언을 어떻게 구분하는지 그 규칙을 정의합니다.

5.1.1. 새 선언: let

let_stmt    ::= "let" let_lhs (":" type_expr)? "=" expr ";"
let_lhs     ::= identifier
              | irrefutable_pattern    // tuple / struct destructure - 5.1.11 참조

가장 흔히 쓰이는 형태는 단일 식별자입니다 (let x = ...). 좌변에 반증 불가능 패턴 (5.1.11) 을 두면 분해 대입 이 됩니다. let-else 형태 (let-else) 는 별도의 문법으로 다루고 있습니다.

let x = 42i32;
let y: i32 = 10i32;
let z;                            // ERROR - 초기값 없음
let z: i32;                        // ERROR - 초기값 없음

기본 구성은 let 다음에 이름을 적고, 필요하다면 : 타입 을 덧붙인 뒤, = 와 초기값을 차례로 씁니다. 이때 초기값은 반드시 함께 적어야 하며, 미초기화 상태의 변수는 도입하지 않습니다. 타입을 명시하면 그 타입의 정적 슬롯이 되고, 명시하지 않으면 초기값의 타입으로 추론된 정적 슬롯이 됩니다 (5.1.7). any 슬롯 은 : any 를 명시할 때만 생깁니다.

이렇게 하면 값이 채워지지 않은 슬롯을 읽게 되는 함정 (Rust 의 borrow checker 가 차단하는 종류의 문제) 을 원천적으로 막을 수 있습니다. 만약 초기값을 미리 정해 둘 수 없는 상황이라면, 합 타입 의 T | none 을 사용하여 부재 상태를 명시하면 됩니다.

let cache: Data | none = none;     // "아직 없음" 을 명시
cache = compute();                  // 나중에 채움

5.1.2. 재할당: let 없이

이미 선언된 변수에 다른 값을 다시 대입할 때에는 let 을 빼고 적으면 됩니다.

let counter: i32 = 0i32;
counter = counter + 1i32;         // OK - 재할당
counter = counter + 1i32;         // OK - 또 재할당

선언된 적이 없는 이름에 let 없이 곧바로 대입하면 컴파일 에러가 발생합니다.

let counter: i32 = 0i32;
coutner = counter + 1i32;         // ERROR: 'coutner' not declared.
                                   //        Did you mean 'counter'?

이는 Julia 와 Python 에서 흔히 마주치게 되는, 오타가 곧 새로운 변수를 만들어 버리는 함정을 차단하기 위한 장치입니다.

5.1.3. 새 선언과 재할당의 결정

형태의미
let name = ...;새 선언. 그 스코프에 새 바인딩.
name = ...;재할당. 기존 바인딩의 슬롯에 다시 쓰기.

오타가 났을 때 컴파일 에러를 발생시켜 명시성을 우선하기 위함입니다.

5.1.4. 같은 스코프 내 재선언 금지

같은 스코프 안에서 같은 이름을 다시 선언하면 컴파일 에러가 됩니다.

let x = 1;
let x: i32 = 5i32;                  // ERROR - 같은 스코프에 x 가 이미 있음

변수의 슬롯 타입은 선언 시점에 고정되며, 같은 스코프 안에서는 바뀌지 않습니다.

예외는 REPL 과 Jupyter 셀의 최상위입니다. 셀 재실행 흐름을 지원하기 위해, 셀 최상위에서는 같은 이름을 다시 let 으로 선언할 수 있고 이는 새 바인딩을 만듭니다 (0.1, 9.7.3). 일반 소스 파일에는 이 예외가 적용되지 않습니다.

5.1.5. 스코프: 블록 단위

변수가 보이는 범위는 블록 표현식 의 블록 단위로 결정됩니다.

let y = 1;
if cond {
    y = 2;                         // 외부 y 재할당 (같은 슬롯)
    let z = 2;                     // 블록 로컬 z 선언
}
// 여기서 z 는 접근 불가

블록 안에서 사용된 let 은 그 블록에 속한 새로운 로컬 바인딩이 됩니다. 따라서 블록 바깥에서는 그 변수가 보이지 않게 됩니다. 외부 변수는 같은 이름의 안쪽 let 이 그 블록 안에서만 가리게 됩니다.

5.1.6. 중첩 스코프: 별개 바인딩

같은 이름의 변수가 내부 블록에서 새로 선언되면, 그것은 별개의 바인딩이 됩니다 (shadowing 이 아닙니다).

let a = 1;
{
    let a: i32 = 42i32;            // 블록 로컬 a - 외부 a 와 다른 슬롯
    a = 10i32;                     // 블록 안 a 재할당 (외부 a 영향 없음)
}
// 여기서 a = 1 (외부 a 그대로)

내부 블록의 let 은 완전히 새로운 바인딩을 만들어 줍니다. 외부에 같은 이름이 있더라도 서로 독립된 슬롯이라고 보면 됩니다.

블록을 벗어나면 안쪽 변수가 사라진다는 자연스러운 의미와, 외부 변수를 보호하려는 의도를 동시에 살리려는 설계입니다.

shadowing 과의 차이

Rust 식 shadowing 은 같은 스코프에서 같은 이름을 또다시 선언할 수 있도록 허용합니다. 반면에 rok 은 같은 스코프 안에서의 재선언을 금지합니다 (5.1.4).

내부 블록에서 같은 이름을 새로 선언하는 행위는 shadowing 이 아니라, 새 스코프에서의 별개 바인딩이라고 이해하면 됩니다. 효과만 보면 (블록 안에서 외부 이름이 가려진다는 점) 비슷해 보일 수 있지만, 의미론적으로는 분명히 다릅니다.

5.1.7. 타입 명시와 타입 추론

let a = 42i32;                     // a: i32 (추론)
let b: i32 = 42i32;                // b: i32 (명시)
let c: any = 42i32;                // c: any (명시)
let d = if cond { 1i32 } else { 2i32 };   // d: i32 (추론)

타입을 명시하면 그 타입의 정적 슬롯이 되고, 명시하지 않으면 초기값 식의 타입으로 추론된 정적 슬롯이 됩니다 (지역 타입 추론). 이후의 재할당은 그 타입으로 검사합니다. any 슬롯 은 : any 를 명시할 때만 생깁니다.

let n = 1i32;
n = 2i32;                          // OK
n = "hi";                          // ERROR - n 은 i32

초기값에서 타입이 다 정해지지 않으면 변수의 스코프 안 이후 쓰임 (값을 타입이 정해진 자리로 넘기는 쓰임) 에서 마저 추론합니다 (2.10.2). 끝까지 정할 수 없으면 컴파일 에러가 되며, 이때는 타입을 명시해야 합니다.

let x = none;                      // ERROR - 타입을 정할 수 없음
let x: Data | none = none;         // OK

let xs = [];                       // 이후 쓰임이 없으면 ERROR - 원소 타입을 정할 수 없음
let xs: List<i32> = [];            // OK

let y = return;                    // ERROR - 초기값이 발산만 함

분기의 타입이 서로 다른 if / match 식으로는 타입을 추론하지 않습니다. 합 타입이 필요하면 슬롯에 명시합니다.

let e = if cond { 1i32 } else { "hi" };          // ERROR - 분기 타입 불일치
let e: i32 | str = if cond { 1i32 } else { "hi" };   // OK

추론의 결과 타입과 분기 결과 규칙에 대한 자세한 내용은 표현식 개요 를 참조하세요.

5.1.8. 동시 선언 (없음)

let a = 1, b = 2;                  // 채택 안 함

C 식으로 콤마를 사용해 여러 변수를 한꺼번에 선언하는 형태는 도입하지 않습니다. 한 줄에 하나씩 적거나, 분해 대입 의 튜플 분해를 활용하세요.

let (a, b) = (1, 2);               // 분해 대입 - OK
let a = 1; let b = 2;              // 두 줄로 - OK

5.1.9. let 의 자리

let 은 문장 (statement) 입니다 (표현식 개요 의 변환 규칙을 참고하세요).

모듈 수준에서는 let 을 허용하지 않습니다 (모듈 수준의 값 슬롯은 const 또는 static 으로 선언해야 합니다). 함수 본체나 블록 안에서는 사용할 수 있습니다. if / match / loop 같은 제어 구조의 본체도 그 자체가 블록이므로 마찬가지로 사용 가능합니다. 다만 식 자리에 let 이 직접 등장할 수는 없으며, let 은 식이 아니라 문장이기 때문입니다.

5.1.10. let 으로 선언된 변수의 수명

선언된 시점부터 그 변수가 도입된 스코프가 끝날 때까지 살아 있습니다.

ref 캡처 (캡처 의미론) 를 통해 클로저에 잡힌 변수는, 그 클로저 객체가 살아 있는 동안 추가로 보존됩니다. GC 가 관리하기 때문에 사용자가 직접 해제할 필요는 없습니다.

5.1.11. let 의 좌변과 분해 대입 의 연결

let 의 좌변에는 패턴 가운데 반증 불가능 패턴을 둘 수 있습니다. 이것이 바로 분해 대입 (destructuring) 입니다.

let (x, y) = compute();
let Point { x, y } = origin;

분해 대입의 의미는 분해 대입 (destructuring) 에서 정의해 두었습니다. 각 부분 패턴이 해당 변수에 바인딩되며, 모든 변수가 하나의 스코프에 함께 도입됩니다.

GitHub에서 이 페이지 고치기