읽기 테이블

읽기 테이블 (Readtables)

읽기 테이블(readtable)은 리더(reader)가 문자를 해석하는 방식을 바꾸는 디스패치 테이블이에요. make-readtable로 새 읽기 테이블을 만들고 current-readtable 파라미터로 설치하면 리더의 동작을 확장할 수 있어요.

출처: Racket Reference

본문

13.7.1 읽기 테이블

구분자와 디스패치(Delimiters and Dispatch)의 디스패치 테이블은 기본 읽기 테이블에 대응해요. make-readtable로 새 읽기 테이블을 만들고 current-readtable 파라미터로 설치하면 리더의 동작을 확장할 수 있어요.

읽기 테이블은 리더가 특정 시점에 참고해요.

  • 데이터(datum)의 시작을 찾을 때;
  • #로 시작하는 데이터를 어떻게 파싱할지 결정할 때;
  • 심볼이나 숫자를 끝내는 구분자를 찾을 때;
  • 페어, 리스트, 벡터, 해시 테이블의 수열로 파싱된 첫 문자 뒤의 여는 문자(예: (), 닫는 문자(예: )), 또는 .을 찾을 때; 또는
  • 지정된 길이 ‹n›의 벡터에서 #‹n› 뒤의 여는 문자를 찾을 때.

그 외의 시점에는 읽기 테이블이 무시돼요. 특히 ;의 기본 동작에 매핑된 문자를 파싱한 뒤에는, 주석의 종결 줄 바꿈을 발견할 때까지 읽기 테이블이 무시돼요. 마찬가지로 읽기 테이블은 닫는 큰따옴표를 찾을 때까지 문자열 파싱에 영향을 주지 않아요. 한편, 문자를 (의 기본 동작에 매핑하면, 그 문자는 닫는 괄호 )에 매핑된 어떤 문자로든 닫히는 수열을 시작해요. 겉보기 예외는 |의 기본 파싱이 일치하는 문자를 찾을 때까지 심볼을 인용하지만, 실제로는 파서가 인용을 시작한 문자를 사용할 뿐 읽기 테이블을 참고하지 않는 경우예요.

많은 문맥에서 #f는 기본 읽기 테이블을 식별해요. 특히 #fcurrent-readtable 파라미터의 초깃값이며, 이 경우 리더는 리더(The Reader)에 설명된 대로 동작해요.

procedure

(readtable? v) → boolean?
  v : any/c

v가 읽기 테이블이면 #t를, 그렇지 않으면 #f를 반환해요.

(or/c 'terminating-macro
      'non-terminating-macro
      'dispatch-macro
      char?)

procedure

(make-readtable readtable key mode action ...) → readtable?
  readtable : (or/c readtable? #f)
  key : (or/c char? #f)
  mode :
  action :
    (or/c procedure?
          readtable?
          #f)

readtable(#f는 기본 읽기 테이블을 나타낼 수 있음)과 같지만, 각 key에 대해 리더의 동작이 주어진 modeaction에 따라 수정된 새 읽기 테이블을 만들어요. make-readtable...key, mode, action 세 가지 모두에 적용돼요. 즉 make-readtable의 총 인자 수는 3으로 나누었을 때 나머지가 1이어야 해요.

key, mode, action의 가능한 조합은 다음과 같아요.

  • char 'terminating-macro procchar가 구분자로 파싱되도록 하고, 입력 문자열에서 인용되지 않거나 주석이 아닌 char는 리더 매크로 proc 호출을 촉발해요. proc의 동작은 아래에서 더 자세히 설명해요. 개념적으로 ;, (, ) 같은 문자는 기본 읽기 테이블에서 종결 리더 매크로에 매핑돼요.
  • char 'non-terminating-macro proc'terminating-macro 변형과 같지만, char가 구분자로 취급되지 않아 식별자나 숫자의 중간에 사용될 수 있어요. 개념적으로 #는 기본 읽기 테이블에서 비종결 매크로에 매핑돼요.
  • char 'dispatch-macro proc'non-terminating-macro 변형과 같지만, char# 뒤에 올 때만(더 정확히는, 기본 읽기 테이블에서 #의 동작에 매핑된 문자 뒤에 올 때만) 적용돼요.
  • char like-char readtablecharreadtable에서 like-char가 파싱되는 것과 같은 방식으로 파싱되도록 해요. 여기서 readtable은 기본 읽기 테이블을 나타내는 #f일 수 있어요. (char의 매핑은 # 뒤에는 적용되지 않는데, 이는 'dispatch-macro로 별도로 구성돼요.) 문자를 기본 리더의 |와 같은 동작에 매핑하면 그 문자는 심볼 인용을 시작하고, 같은 문자가 인용을 끝내요. 반대로 문자를 "와 같은 동작에 매핑하면 문자열을 시작하지만, 문자열은 여전히 닫는 "로 끝나요. 마지막으로 문자를 기본 읽기 테이블의 동작에 매핑하면 그 문자의 동작은 원래 문자에 영향을 주는 파라미터에 민감해요. 예를 들어 문자를 기본 읽기 테이블의 중괄호 {와 같은 동작에 매핑하면, read-curly-brace-as-paren 파라미터가 #f로 설정될 때 그 문자가 허용되지 않아요.
  • #f 'non-terminating-macro proc — 특정 매핑이 없는 문자(즉 기본 읽기 테이블로 심볼이나 숫자를 시작할 수 있는 #| 외의 문자)를 파싱하는 데 사용되는 매크로를 대체해요.

하나의 char에 대해 여러 'dispatch-macro 매핑이 제공되면 마지막 것을 제외한 모든 것이 무시돼요. 마찬가지로 하나의 char에 대해 여러 비-'dispatch-macro 매핑이 제공되면 마지막 것을 제외한 모든 것이 무시돼요.

리더 매크로 proc은 여섯 개의 인자를 받아야 하고, 선택적으로 두 개의 인자를 받을 수도 있어요. 처음 두 인자는 항상 리더 매크로를 촉발한 문자와 읽기용 입력 포트예요. 리더 매크로가 read-syntax(또는 read-syntax/recursive)로 촉발되면, 그 프로시저는 이미 소비된 문자의 소스 위치를 나타내는 추가 인자 네 개를 받아요: 소스 이름, 줄 번호 또는 #f, 열 번호 또는 #f, 위치 또는 #f. 리더 매크로가 read(또는 read/recursive)로 촉발되면, 프로시저가 두 인자를 받을 수 있으면 두 인자만 받고, 그렇지 않으면 세 번째가 항상 #f인 여섯 개의 인자를 받아요. 프로시저 결과에 대한 정보는 리더 확장 프로시저(Reader-Extension Procedures)를 보세요.

리더 매크로는 보통 주어진 입력 포트에서 문자를 읽어, 소비된 문자의 "리더 매크로 확장"으로 사용될 값을 만들어 내요. 리더 매크로는 특수 주석 값(특수 주석(Special Comments) 참고)을 만들어 소비된 문자를 공백으로 취급하게 하거나, read/recursiveread-syntax/recursive를 사용할 수 있어요.

(or/c char?
      'terminating-macro
      'non-terminating-macro)

procedure

(readtable-mapping readtable char) → (or/c #f procedure?)
                                      (or/c #f procedure?)
  readtable : readtable?
  char : char?

char에 대한 readtable의 매핑 정보를 만들어 내요. 결과는 세 개의 값이에요.

  • 문자(기본 읽기 테이블에서 그 문자와 같은 동작을 매핑함), 'terminating-macro, 또는 'non-terminating-macro 중 하나. 이 결과는 char의 주요(즉 비-'dispatch-macro) 매핑을 보고해요. 결과가 문자이면 char는 기본 읽기 테이블에서 반환된 문자와 같은 동작에 매핑된 것이에요.
  • #f 또는 리더 매크로 프로시저 중 하나. 첫 결과가 'terminating-macro 또는 'non-terminating-macro일 때 프로시저예요.
  • #f 또는 리더 매크로 프로시저 중 하나. charreadtable에 기본 디스패치 동작을 덮어쓰는 'dispatch-macro 매핑을 가질 때 프로시저예요.

기본 읽기 테이블에 대한 리더 매크로 프로시저는 직접 접근할 수 없다는 점에 주의하세요. 기본 동작을 호출하려면 문자와 #f 읽기 테이블로 read/recursive 또는 read-syntax/recursive를 사용해요.

예시:

> (define (skip-whitespace port)
; Skips whitespace characters, sensitive to the current
; readtable's definition of whitespace
(let ([ch (peek-char port)])
(unless (eof-object? ch)
; Consult current readtable:
(let-values ([(like-ch/sym proc dispatch-proc)
(readtable-mapping (current-readtable) ch)])
; If like-ch/sym is whitespace, then ch is whitespace
(when (and (char? like-ch/sym)
(char-whitespace? like-ch/sym))
(read-char port)
(skip-whitespace port))))))
> (define (skip-comments read-one port src)
; Recursive read, but skip comments and detect EOF
(let loop ()
(let ([v (read-one)])
(cond
[(special-comment? v) (loop)]
[(eof-object? v)
(let-values ([(l c p) (port-next-location port)])
(raise-read-eof-error
"unexpected EOF in tuple" src l c p 1))]
[else v]))))
> (define (parse port read-one src)
; First, check for empty tuple
(skip-whitespace port)
(if (eq? #\> (peek-char port))
null
(let ([elem (read-one)])
(if (special-comment? elem)
; Found a comment, so look for > again
(parse port read-one src)
; Non-empty tuple:
(cons elem
(parse-nonempty port read-one src))))))
> (define (parse-nonempty port read-one src)
; Need a comma or closer
(skip-whitespace port)
(case (peek-char port)
[(#\>) (read-char port)
; Done
null]
[(#\,) (read-char port)
; Read next element and recur
(cons (skip-comments read-one port src)
(parse-nonempty port read-one src))]
[else
; Either a comment or an error; grab location (in case
; of error) and read recursively to detect comments
(let-values ([(l c p) (port-next-location port)]
[(v) (read-one)])
(cond
[(special-comment? v)
; It was a comment, so try again
(parse-nonempty port read-one src)]
[else
; Wasn't a comment, comma, or closer; error
((if (eof-object? v)
raise-read-eof-error
raise-read-error)
"expected `,` or `>`" src l c p 1)]))]))
> (define (make-delims-table)
; Table to use for recursive reads to disallow delimiters
;  (except those in sub-expressions)
(letrec ([misplaced-delimiter
(case-lambda
[(ch port) (misplaced-delimiter ch port #f #f #f #f)]
[(ch port src line col pos)
(raise-read-error
(format "misplaced `~a` in tuple" ch)
src line col pos 1)])])
(make-readtable (current-readtable)
#\, 'terminating-macro misplaced-delimiter
#\> 'terminating-macro misplaced-delimiter)))
> (define (wrap l)
`(make-tuple (list ,@l)))
> (define parse-open-tuple
(case-lambda
[(ch port)
; 'read' mode
(wrap (parse port
(lambda ()
(read/recursive port #f
(make-delims-table)))
(object-name port)))]
[(ch port src line col pos)
; 'read-syntax' mode
(datum->syntax
#f
(wrap (parse port
(lambda ()
(read-syntax/recursive src port #f
(make-delims-table)))
src))
(let-values ([(l c p) (port-next-location port)])
(list src line col pos (and pos (- p pos)))))]))
> (define tuple-readtable
(make-readtable #f #\< 'terminating-macro parse-open-tuple))
> (parameterize ([current-readtable tuple-readtable])
(read (open-input-string "<1 , 2 , \"a\">")))

'(make-tuple (list 1 2 "a"))

> (parameterize ([current-readtable tuple-readtable])
(read (open-input-string
"< #||# 1 #||# , #||# 2 #||# , #||# \"a\" #||# >")))

'(make-tuple (list 1 2 "a"))

> (define tuple-readtable+
(make-readtable tuple-readtable
#\* 'terminating-macro (lambda a
(make-special-comment #f))
#\_ #\space #f))
> (parameterize ([current-readtable tuple-readtable+])
(read (open-input-string "< * 1 __,__  2 __,__ * \"a\" * >")))

'(make-tuple (list 1 2 "a"))

더 알아보기