2.7장에서 들여쓰기 규칙을 간단히 설명했습니다. 이번 장에서 더 자세히 정의합니다.
하스켈 프로그램의 의미는 들여쓰기에 따라 달라집니다. 중괄호와 세미콜론을 적절히 추가하는 것으로 들여쓰기의 효과를 완전히 갈음할 수 있습니다. 그렇게 한 프로그램은 들여쓰기할 필요가 없어집니다.
들여쓰기된 프로그램에 어떻게 중괄호와 세미콜론을 추가하는지 설명해서 들여쓰기의 효과를 설명하겠습니다. 들여쓰기 변환 함수 L을 정의하겠습니다. L의 입력은:
-
아래 토큰이 추가된 하스켈 레포트 어휘 구문에 정의된 어휘소lexeme 스트림:
let
,where
,do
,of
키워드 뒤에{
가 없으면{n}
토큰이 그 키워드 뒤에 들어갑니다. n은 다음 어휘소의 들여쓰기 수준을 의미하고 파일의 끝일 때는 0이 됩니다.- 모듈의 처음 어휘소가
{
이나module
이 아니었다면 어휘소의 들여쓰기 수준 n을 포함한{n}
을 앞에 삽입합니다. - 한 줄에서 어휘소의 시작 부분 앞에 공백밖에 없다면, 그 어휘소 앞에 어휘소의 들여쓰기 수준 n을 포함한
<n>
을 삽입합니다. 그렇지 않다면 앞의 두 규칙에 의해 앞에{n}
을 삽입합니다. (참고: 문자열 상수는 여러 줄을 차지할 수 있습니다. 2.6장 참고하시고, 그래서 아래 코드에서f = ("Hello \ \Bill", "Jake")
<n>
은\Bill
이나,
앞에 삽입되지 않는데 완전한 어휘소의 시작이 아니거나, 앞의 공백만 있는 게 아니기 때문입니다.)
-
아래 원소를 가진 "들여쓰기 문맥" 스택:
- 어디까지가 범위인지 확실함을 나타내는 0 (즉, 프로그래머가 {를 사용한 경우). 만약 가장 안쪽의 문맥이 0이면 문맥이 끝나거나 새 들여쓰기 문맥이 시작할 때까지 들여쓰기 토큰은 삽입되지 않습니다.
- 들여쓰기 문맥의 들여쓰기 열 수를 나타내는 양수.
어휘소의 "들여쓰기"는 그 어휘소의 첫 번째 문자가 있는 열의 번호입니다. 줄의 들여쓰기는 그 줄의 가장 왼쪽에 있는 어휘소의 들여쓰기 값입니다. 열의 번호는 고정폭 글꼴을 가정할 때 아래 관습을 따릅니다.
- CR, LF, CR/LF 모두 새 줄을 시작합니다.
- 첫 번째 열은 0번이 아니라 1번입니다.
- 탭 위치는 8 문자 간격으로 떨어져 있습니다.
- 탭 문자는 다음 탭 위치까지 공백을 넣는 역할을 합니다.
들여쓰기 규칙에서 소스의 유니코드 문자는 아스키 문자와 동일한 고정폭을 가진다고 간주합니다. 하지만 시각적 혼란을 막기 위해서라도 프로그래머는 공백이 아닌 문자의 폭에 따라 들여쓰기가 달라질 수 있는 프로그램을 짜는 걸 피해야 합니다.
L tokens []
프로그램은 위에 나온 규칙대로 모듈을 분석해 열 번호를 붙인 들여쓰기 토큰을 추가한 들여쓰기 인식이 필요없는 토큰들을 생성합니다. L의 정의는 다음과 같은데, :
는 스트림 추가 연산자고, []
는 빈 스트림을 의미합니다.
L (<n> : ts) (m : ms) = ; : (L ts (m : ms)) if m = n = } : (L (<n> : ts) ms) if n < m L (<n> : ts) ms = L ts ms L ({n} : ts) (m : ms) = { : (L ts (n : m : ms)) if n > m (노트 1) L ({n} : ts) [] = { : (L ts [n]) if n > 0 (노트 1) L ({n} : ts) ms = { : } : (L (<n> : ts) ms) (노트 2) L (} : ts) (0 : ms) = } : (L ts ms) (노트 3) L (} : ts) ms = parse-error (노트 3) L ({ : ts) ms = { : (L ts (0 : ms)) (노트 4) L (t : ts) (m : ms) = } : (L (t : ts) ms) if m ≠ 0 and parse-error(t) (노트 5) L (t : ts) ms = t : (L ts ms) L [] [] = [] L [] (m:ms) = } : L [] ms if m ≠ 0 (노트 6)
노트 1.
중첩된 문맥은 그걸 포함한 문맥보다 더 들여쓰기 해야 합니다 (n > m). 그렇지 않으면 L은 실패하고, 컴파일러는 들여쓰기 에러를 알려줘야 합니다. 예를 들어:
f x = let
h y = let
p z = z
in p
in h
여기서 p
의 정의는 그걸 감싼 h
를 정의하는 문맥보다 덜 들여쓰기 되어있고, 이건 에러입니다.
노트 2.
만약 where
다음의 첫 번째 토큰이 그걸 감싸는 문맥보다 덜 들여쓰기 되어있다면, 블록은 비게 되고 빈 중괄호 짝이 삽입됩니다. {n}
토큰은 빈 중괄호 짝이 실제 나온 것처럼 보이기 위해 <n>
으로 바뀝니다.
노트 3.
0이란 들여쓰기 값을 매칭에 사용함으로써, 명시한 }
가 명시한 {
에만 대응할 수 있도록 보장할 수 있습니다. 명시한 }
가 암시적인 {
에 대응하면 파싱 에러가 됩니다.
노트 4.
이 절은 레코드 구문(3.15장)을 포함한 모든 중괄호 짝이 명시된 들여쓰기 문맥처럼 취급되어야 함을 뜻합니다. This is a difference between this formulation and Haskell 1.4.
노트 5.
조건인 parse-error(t)의 해석은 다음과 같습니다: 지금까지 L이 생성한 토큰과 다음 토큰 t를 합쳤을 때 하스켈 문법의 올바른 앞 부분이 되지 않고, 대신 }
를 붙이면 말이 될 때 parse-error(t)는 참입니다. m ≠ 0 조건은 암시적으로 생성한 }
가 암시적인 {
에 대응하는지 확인합니다.
노트 6.
입력이 끝나면 모든 미뤄진 }
가 삽입됩니다. 이 시점에서 들여쓰기 무시 문맥에 있는 건 에러입니다 (즉 m = 0일 때).
만약 위 규칙 중 아무 것에도 해당하지 않는다면 들여쓰기 알고리즘은 실패합니다. 가령 입력의 끝인데 들여쓰기 무시 문맥일 경우엔 닫는 괄호가 없는 것이기에 실패할 수 있습니다. let }
같은 알고리즘으로 탐지하게 할 순 있었겠지만 탐지되지 않는 일부 에러조건도 있습니다.
노트 1은 들여쓰기 처리를 파싱 에러로 빨리 중단할 수 있는 기능이기도 합니다. 예를 들어
let x = e; y = x in e'
는 아래와 같기 때문에 적법합니다.
let { x = e; y = x } in e'
}
는 노트 5의 규칙이 노트 1의 규칙으로 파싱 에러를 판단해 삽입되었습니다.