Skip to content

Instantly share code, notes, and snippets.

@jeiea
Created December 27, 2018 04:45
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jeiea/e71f782912a5fcd6546405bbc66e68ca to your computer and use it in GitHub Desktop.
Save jeiea/e71f782912a5fcd6546405bbc66e68ca to your computer and use it in GitHub Desktop.
하스켈 2010 레포트 들여쓰기 규칙을 필요에 의해 번역함

10.3 레이아웃(이하 들여쓰기)

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의 규칙으로 파싱 에러를 판단해 삽입되었습니다.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment