SICPを読む前に: Scheme 編

普段 Scheme を使わないプログラマ向けに SICP を快適に読むための文書です。 社内で実施する SICP 輪読会の準備のために作成しました。

前提: SICP は Scheme の教科書ではない

SICP は計算機科学の入門書であって Scheme について学ぶ本ではありません。 実際、SICP では Scheme の初歩的な言語機能しか説明していないので Scheme の教科書としては不十分だと思われます。

どうして SICP を読むのに Scheme を使う必要があるのか

SICP は5章編成なのですが、少なくとも3章までは Scheme 以外の多くのプログラミング言語でも通用する一般的な内容です。 ただし、SICP は Scheme 使って読むことが前提となっていて第3章までであっても Scheme 以外のプログラミング言語を使用して SICP を読もうとするといくつかの障壁があります。

私がぱっと思いつくのは下記の3点です。

  • SICP では対(pair)というデータ構造を使用する
    • Python や Haskell にある tuple とは異なり、Scheme では pair を組み合わせて list という構造を構成する
      • pair を組み合わせて list を構成するようになっているプログラミング言語は少ない
        • Lisp 方言の一つである Clojure はリストを構成するのに pair を使用していない
    • Scheme の pair は破壊できる (set-car!, set-cdr!)
      • Lisp と同じようなリスト構造を扱えるプログラミング言語で、その構成要素の pair を破壊できるプログラミング言語は少ない(私は知らない)
  • SICP では末尾呼び出しが最適化されることを前提としている
    • 末尾呼び出しが最適化されないプログラミング言語では、SICP に紹介されているようなプログラムの書き方をするのはスタイルとして良くない
      • 関数型言語の多くで末尾呼び出しが最適化されるが、そのような関数型言語には Scheme の pair がない
    • Scheme 以外の Lisp 方言の多くは末尾呼び出しの最適化が保証されていない
  • SICP の 3.5 章を読むのに cons-stream が実装できないといけない
    • これは特殊形式を追加できるプログラミング言語でないと書けない、マクロが書けないと詰んでしまう

正直なところ全て些細な問題なのですが、他のプログラミング言語で SICP を読もうとした場合に障壁となるので素直に Scheme を使った方が良いです。

エディタについて

pair と末尾呼び出しの最適化と cons-stream を除けば SICP では Scheme 特有の特殊な仕組みを必要としません。 よって SICP で解説されるほとんどの言語機能は現代の他のプログラミング言語にもあるものが多く、そこで躓くことはないと思います。 (とくに Ruby や JavaScript といったプログラミング言語を使ったことがあれば、新しいものはほとんどないはずです)

ただし、Scheme というか Lisp の構文は他のプログラミング言語と大きく異なっているので注意が必要です。 とくにインデントのルールが特殊でエディタの支援なしに正しくインデントをすることは困難です。

よって、Scheme のインデントが正しくできるエディタを使用することを強く推奨します。

私が推奨するエディタ

  1. Emacs

    個人的には Emacs の使用を強く推奨します。 Emacs を使用している Schemer が多いので Emacs で Scheme を使うための環境構築をするためのドキュメントがたくさん存在しています。 また、私は Emacs で Scheme をよく書いているので、何か問題が生じてもすぐに対応できる可能性が高いです。

  2. DrRacket

    Emacs を使用しない場合は DrRacket がおすすめです。 DrRacket を使うと実質的に選択できる Scheme の処理系が Racket に絞られてしまうのが難点なのですが SICP を読む限りは大きな問題にはならないです。

    ただし、Racket の標準の言語(#lang racket)では set-car!, set-cdr! が存在していないので普通に使うと第3章で詰みます。 #lang r5rs とするか DrRacket の設定を変更して標準の Scheme を使用するように変更する必要があります。 また、Racket には sicp 専用のモードがあるようなのでそちらを使用してもいいかもしれません。

推奨していいのかよく分かっていないエディタ

  1. Vim

    軽く調べた感じ Vim でも Lisp のプログラムが書けるようです。 if のインデントが Scheme の一般的なスタイルとは違ったので、 lispwords 変数から if は取り除いた方がいいかもしれません。

    また、Vim から REPL を起動できるのかといったことについては良く分かっていないので、Vim を使う場合は自己責任でお願いします。

非推奨なエディタ

  1. Visual Studio Code

    Scheme のコードをハイライトするプラグインを見つけることはできたのですが、 正しくインデントするためのプラグインを見つけることができませんでした(良いプラグインを知っている方がいたら教えて欲しいです。Twitter の tojoqk にリプライか DM もしくはメールで連絡をお願いいたします)。

    次の章で説明しますが、正しく自動でインデントができないと Scheme のプログラムを書くのはかなり厳しいのでインデントの問題が解決しない限りは諦めて欲しいです……。

初めて Scheme を書く人が遭遇するエラーと対処法

他に何か思いついたら追記していきます。

括弧の対応を間違えてしまう

Scheme のコードで (f a b c) というような式が現われた場合、 基本的には f が手続きであれば a, b, c を渡して f を呼びだすという意味で、 f が特殊形式であれば (f a b c)f 特有のルールで評価されます。

これだけであれば簡単なのですが例外があって letcond のような特殊形式の中では括弧をグルーピングのために使用しています。 グルーピングのために使用された括弧の中で手続きを呼び出すような場合には括弧をたくさん書く必要があり、ここで括弧の対応を誤ってしまうという問題が発生しがちです。

たとえば下記のように let の中で括弧の対応を誤ってしまう場合があります(ここでは let についての説明はしません)

(let ((a (+ 1 2)
      (b (+ 3 4))))
  (+ a b))

これを Scheme に解釈させると、 let の構文エラーになるのですが、 ぱっと見ただけではどこがおかしいのかを判断するのは困難です。 Scheme 側も let の中でどういった構文エラーが発生しているかまでは伝えることができるのですが、 具体的にどう直せばよいのかまでは教えてくれません。 よって正しい対処法を知らないとこのような些細なミスで多くの時間を浪費してしまいます。

こういった場合にはエディタの機能で let 式全体を再インデント することによってエラーの箇所を特定することができます。 今回のコードを再インデントをすると下記のような状態になります。

(let ((a (+ 1 2)
         (b (+ 3 4))))
  (+ a b))

(b (+ 3 4)) が右にずれてしまいました。 これは (a (+ 1 2)) しないといけないのに最後の閉じ括弧を書き忘れてしまっていることが原因です。

このように、エディタの 自動インデント機能は重要 です。 括弧の対応に関するエラーはほとんど自動インデントで撲滅できるのでうまく活用してください。

これさえ気をつければ、「Scheme には括弧がいっぱいあって大変」ということはないので安心してプログラムを書くことができます。