SICPを読む前に: Scheme 編
目次
1. 警告
に追記
この記事はまったく有用ではありません(そのため現在は検索にかからないように noindex を付けています)。 Emacs 以外で Scheme を使う方法についてはよく探してみてください。
自分が Emacs を使っていて他のエディタを使うモチベが全くないため、 Emacs 以外で Scheme を書く方法について調査することが難しくこの記事を更新することもできません。
2. はじめに
普段 Scheme を使わないプログラマ向けに SICP を快適に読むための文書です。 社内で実施する SICP 輪読会の準備のために作成しました。
3. 前提: SICP は Scheme の教科書ではない
SICP は計算機科学の入門書であって Scheme について学ぶ本ではありません。 実際、SICP では Scheme の初歩的な言語機能しか説明していないので Scheme の教科書としては不十分だと思われます。
4. どうして 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 を破壊できるプログラミング言語は少ない(私は知らない)
- Python や Haskell にある
- SICP では末尾呼び出しが最適化されることを前提としている
- 末尾呼び出しが最適化されないプログラミング言語では、SICP
に紹介されているようなプログラムの書き方をするのはスタイルとして良くない
- 関数型言語の多くで末尾呼び出しが最適化されるが、そのような関数型言語には Scheme の pair がない
- Scheme 以外の Lisp 方言の多くは末尾呼び出しの最適化が保証されていない
- 末尾呼び出しが最適化されないプログラミング言語では、SICP
に紹介されているようなプログラムの書き方をするのはスタイルとして良くない
- SICP の 3.5 章を読むのに cons-stream が実装できないといけない
- これは特殊形式を追加できるプログラミング言語でないと書けない、マクロが書けないと詰んでしまう
正直なところ全て些細な問題なのですが、他のプログラミング言語で SICP を読もうとした場合に障壁となるので素直に Scheme を使った方が良いです。
5. エディタについて
pair と末尾呼び出しの最適化と cons-stream を除けば SICP では Scheme
特有の特殊な仕組みを必要としません。 よって SICP
で解説されるほとんどの言語機能は現代の他のプログラミング言語にもあるものが多く、そこで躓くことはないと思います。
(とくに Ruby や JavaScript
といったプログラミング言語を使ったことがあれば、新しいものはほとんどないはずです)
ただし、Scheme というか Lisp の構文は他のプログラミング言語と大きく異なっているので注意が必要です。 とくにインデントのルールが特殊でエディタの支援なしに正しくインデントをすることは困難です。
よって、Scheme のインデントが正しくできるエディタを使用することを強く推奨します。
5.1. 私が推奨するエディタ
Emacs
個人的には Emacs の使用を強く推奨します。 Emacs を使用している Schemer が多いので Emacs で Scheme を使うための環境構築をするためのドキュメントがたくさん存在しています。 また、私は Emacs で Scheme をよく書いているので、何か問題が生じてもすぐに対応できる可能性が高いです。
DrRacket
Emacs を使用しない場合は DrRacket がおすすめです。 DrRacket を使うと実質的に選択できる Scheme の処理系が Racket に絞られてしまうのが難点なのですが SICP を読む限りは大きな問題にはならないです。
ただし、Racket の標準の言語(
#lang racket)ではset-car!,set-cdr!が存在していないので普通に使うと第3章で詰みます。#lang r5rsとするか DrRacket の設定を変更して標準の Scheme を使用するように変更する必要があります。 また、Racket には sicp 専用のモードがあるようなのでそちらを使用してもいいかもしれません。
5.2. 推奨していいのかよく分かっていないエディタ
Vim
軽く調べた感じ Vim でも Lisp のプログラムが書けるようです。
ifのインデントが Scheme の一般的なスタイルとは違ったので、lispwords変数からifは取り除いた方がいいかもしれません。また、Vim から REPL を起動できるのかといったことについては良く分かっていないので、Vim を使う場合は自己責任でお願いします。
5.3. 非推奨なエディタ
Visual Studio Code
Scheme のコードをハイライトするプラグインを見つけることはできたのですが、 正しくインデントするためのプラグインを見つけることができませんでした(良いプラグインを知っている方がいたら教えて欲しいです。Twitter の tojoqk にリプライか DM もしくはメールで連絡をお願いいたします)。
次の章で説明しますが、正しく自動でインデントができないと Scheme のプログラムを書くのはかなり厳しいのでインデントの問題が解決しない限りは諦めて欲しいです……。
6. 初めて Scheme を書く人が遭遇するエラーと対処法
他に何か思いついたら追記していきます。
6.1. 括弧の対応を間違えてしまう
Scheme のコードで (f a b c) というような式が現われた場合、 基本的には
f が手続きであれば a, b, c を渡して f を呼びだすという意味で、
f が特殊形式であれば (f a b c) が f 特有のルールで評価されます。
これだけであれば簡単なのですが例外があって let や cond
のような特殊形式の中では括弧をグルーピングのために使用しています。
グルーピングのために使用された括弧の中で手続きを呼び出すような場合には括弧をたくさん書く必要があり、ここで括弧の対応を誤ってしまうという問題が発生しがちです。
たとえば下記のように 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 には括弧がいっぱいあって大変」ということはないので安心してプログラムを書くことができます。
