表題

インデントにより構造化する構文

著者

Egil Mler
http://redhog.org/ / redhog@redhog.org

著者

この SRFI は現在「確定」の状態である。 SRFI の各状態の説明については ここ を参照せよ。 この SRFI に対するコメントは、 srfi minus 49 at srfi dot schemers dot org 宛てにメールで送信してください。 このメーリングリストに参加する方法については、 ここの説明 を参照してください。 この SRFI に関する議論については メーリングリストのアーカイブ を参照せよ。 この SRFI に関する確定後の議論については メーリングリストのアーカイブ を参照せよ。

概要

この SRFI では、S-式と同等の記述力を持つ、 I-式 (I-expression) と呼ばれる Scheme の新しい構文について述べる。 この構文は式をグループ化するためにインデントを使用するだけで、 言語の意味論的な側面については関与しない。 この構文はプログラムとデータ入力の両方で使うことができる。

また、S-式と I-式を自由に混在させることができるので、 プログラマは可読性を最大化するようにコードをレイアウトすることができる。

論拠

これまで、さまざまな LISP 方言のために、 いくつかの 非S-式の構文が作られ、そして捨てられてきた。 しかし、人々は S-式に対してそれほど満足してはいないようだ。 特に初心者でない者はそうである。 彼らは常々括弧の多さに不満を漏らしているので、 新しい構文が発明され、時が経つと共に捨てられてきたのである。 過去に発明されてきた構文には、共通な性質が1つあったが、 それは S-式にはない性質であった。 それらの構文は LISP 方言のある種の意味論をサポートするための 特殊な構文を備えていたのである。

多くの言語では、文や式をグループ化するために、括弧、ブレース、ブラケットを使用する。 また、余分なスペースや改行を入れることで、文や式が見やすくなるようにレイアウトする。 しかしこれは、しばしばプログラマの落とし穴になる。 インデントがあると、括弧、ブレース、ブラケットが変更された場合でも、 そのコードがグループ化されているように捉えられてしまうからである。

最近、Python 言語が普及してきたことで、 インデントのみでグループ化する方法が知られるようになってきた。 この方法は、インデントが言語本来のグループ化の方法と 同期されないという問題を解決する。 残念なことに、Python の構文は、 言語のさまざまな意味論をサポートするために特別な構文を使用している。 また、ファイル入力と対話的入力では、その構文が微妙に異なる。 それに加えて、Python のインデント構文は文のみを対象としており、 式やデータに対しては機能しない。

仕様

ファイルの各行は空行 (空白やコメントのみを含む行) であるか、 あるいは、いくつかのスペースやタブに続くコードを含む行である。

以下に示した構文定義では、 この行頭のスペースや改行は、ルールには含まれない。 その代わり、各行の先頭のスペースは、 直前の非空行の先頭のスペースと比較され、 除去される。 もし、直前の非空行よりもスペースが多いなら、 行頭に INDENT という特殊な記号が追加される。 もし、直前の非空行よりもスペースが少ないなら、 行頭に DEDENT という特殊な記号が追加される。 現在の行と直前の非空行の行頭にあるスペース/タブのシーケンスが、 プレフィックスの関係にあるか、等しい場合以外は、エラーである。

特殊な非終端記号 sexpr は、任意の有効な S-式に展開される。 特殊な終端記号 GROUP は入力ストリームにおいて "group" という単語に展開される。 GROUP 記号は、最初の要素がリストであるリストを許容するために使われる。 これは空行のインデントが無視されるがために必要となる。

以下の各生成ルールは、 そのルールにマッチする式の値を計算する。 これらのルールでは、記号 $1 ... $n は マッチする 1番目... n番目の部分式に置換される。

  expr -> QUOTE expr
   (list 'quote $2)
  expr -> QUASIQUOTE expr
   (list 'quasiquote $2)
  expr -> UNQUOTE expr
   (list 'unquote $2)

  expr -> head INDENT body DEDENT
   (append $1 $3)
  expr -> GROUP head INDENT body DEDENT
   (append $2 $4)
  expr -> GROUP INDENT body DEDENT
   $3
  expr -> head
   (if (= (length $1) 1)
       (car $1)
     $1)
  expr -> GROUP head
   (if (= (length $2) 1)
       (car $2)
     $2)

  head-> expr head
   (append $1 $2)
  head-> expr
   (list expr)

  body -> expr body
    (cons $1 $2)
  body ->
   '()
  

使用例

  define
   fac x
   if
    = x 0
    1
    * x
      fac
       - x 1

  let
   group
    foo
     + 1 2
    bar
     + 3 4
   + foo bar

  上記のコードと等価であるが、従来の S-式を用いてコードを密集させた記法:

  define (fac x)
   if (= x 0) 1
    * x
     fac (- x 1)

  let
   group
    foo (+ 1 2)
    bar (+ 3 4)
   + foo bar
  

実装

以下のコードは GNU Guile のモジュールとして I-式を実装している。 このモジュールは (use-modules (sugar)) にロードすることができる。
  ----{ sugar.scm }----
  (define-module (sugar))

  (define-public group 'group)

  (define-public sugar-read-save read)
  (define-public sugar-load-save primitive-load)

  (define (readquote level port qt)
    (read-char port)
    (let ((char (peek-char port)))
      (if (or (eq? char #\space)
	      (eq? char #\newline)
	      (eq? char #\ht))
	  (list qt)
	  (list qt (sugar-read-save port)))))

  (define (readitem level port)
    (let ((char (peek-char port)))
      (cond
       ((eq? char #\`)
	(readquote level port 'quasiquote))
       ((eq? char #\')
	(readquote level port 'quote))
       ((eq? char #\,)
	(readquote level port 'unquote))
       (t
	(sugar-read-save port)))))

  (define (indentation>? indentation1 indentation2)
    (let ((len1 (string-length indentation1))
	  (len2 (string-length indentation2)))
      (and (> len1 len2)
	   (string=? indentation2 (substring indentation1 0 len2)))))

  (define (indentationlevel port)
    (define (indentationlevel)
      (if (or (eq? (peek-char port) #\space)
	      (eq? (peek-char port) #\ht))
	  (cons
	   (read-char port)
	   (indentationlevel))
	  '()))
    (list->string (indentationlevel)))

  (define (clean line)
    (cond
     ((not (pair? line))
      line)
     ((null? line)
      line)
     ((eq? (car line) 'group)
      (cdr line))
     ((null? (car line))
      (cdr line))
     ((list? (car line))
      (if (or (equal? (car line) '(quote))
	      (equal? (car line) '(quasiquote))
	      (equal? (car line) '(unquote)))
	  (if (and (list? (cdr line))
		   (= (length (cdr line)) 1))
	      (cons
	       (car (car line))
	       (cdr line))
	      (list
	       (car (car line))
	       (cdr line)))
	  (cons
	   (clean (car line))
	   (cdr line))))
     (#t
      line)))

  ;; Reads all subblocks of a block
  (define (readblocks level port)
    (let* ((read (readblock-clean level port))
	   (next-level (car read))
	   (block (cdr read)))
      (if (string=? next-level level)
	  (let* ((reads (readblocks level port))
		 (next-next-level (car reads))
		 (next-blocks (cdr reads)))
	    (if (eq? block '.)
		(if (pair? next-blocks)
		    (cons next-next-level (car next-blocks))
		    (cons next-next-level next-blocks))
		(cons next-next-level (cons block next-blocks))))
	  (cons next-level (list block)))))

  ;; Read one block of input
  (define (readblock level port)
    (let ((char (peek-char port)))
      (cond
       ((eof-object? char)
	(cons -1 char))
       ((eq? char #\newline)
	(read-char port)
	(let ((next-level (indentationlevel port)))
	  (if (indentation>? next-level level)
	      (readblocks next-level port)
	      (cons next-level '()))))
       ((or (eq? char #\space)
	    (eq? char #\ht))
	(read-char port)
	(readblock level port))
       (t
	(let* ((first (readitem level port))
	       (rest (readblock level port))
	       (level (car rest))
	       (block (cdr rest)))
	  (if (eq? first '.)
	      (if (pair? block)
		  (cons level (car block))
		  rest)
	      (cons level (cons first block))))))))

  ;; reads a block and handles group, (quote), (unquote) and
  ;; (quasiquote).
  (define (readblock-clean level port)
    (let* ((read (readblock level port))
	   (next-level (car read))
	   (block (cdr read)))
      (if (or (not (list? block)) (> (length block) 1))
	  (cons next-level (clean block))
	  (if (= (length block) 1)
	      (cons next-level (car block))
	      (cons next-level '.)))))

  (define-public (sugar-read . port)
    (let* ((read (readblock-clean "" (if (null? port)
					(current-input-port)
					(car port))))
	   (level (car read))
	   (block (cdr read)))
      (cond
       ((eq? block '.)
	'())
       (t
	block))))

  (define-public (sugar-load filename)
    (define (load port)
      (let ((inp (read port)))
	(if (eof-object? inp)
	    #t
	    (begin
	      (eval inp)
	      (load port)))))
    (load (open-input-file filename)))

  (define-public (sugar-enable)
    (set! read sugar-read)
    (set! primitive-load sugar-load))

  (define-public (sugar-disable)
    (set! read sugar-read-save)
    (set! primitive-load sugar-load-save))

  (sugar-enable)
  ----{ sugar.scm }----
  

著作権

Copyright (C) 2005 by Egil Mler . All Rights Reserved.

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


編集者: Mike Sperber