補足 (1. はじめに)


Common Lisp
Common Lisp は、Lisp の処理系の中で最も広く使われているものの一つです。 多種多様な Macro 群 (ある種の library の様なもの) が提供されており強力ですが、 その反面仕様書が電話帳の様になってしまい、はじめは慣れないかもしれません。

scheme との違いは、syntax が少し異なる事と、 Scheme の static scoping に対して、 Common Lisp の scope が dynamic であること等が あげられます。また、より大きな違いとして、 Common Lisp 上の programming style では代入を頻繁に用いる事が多いです。

Common Lisp の拡張として、オブジェクト指向のCLOS (Common Lisp Object System) なども存在します。

Emacs Lisp
Emacs Lisp は、別の意味で最も頻繁に用いられているかもしれない Lisp の一つです。 というのも、emacs/mule は Emacs Lisp 上で動いているからです。

一度は、 ~/.emacs という file をみたことがあると思いますが、これは Emacs Lisp で記述されています。

Emacs Lisp は Emacs 上で簡単に使うことができます。 例えば、

  1. なにか buffer を準備し、 (C-x C-f tmp)
  2. Lisp Interaction Mode にし、(M-x lisp-interaction-mode)
  3. (+ 3 (* 2 6)) などと 式を記述し、
  4. 最後の")" のある行の、 ")"の右側にカーソルをもっていき、
  5. 式を評価する (C-j) (もしくは、M-x eval-print-last-sexp)
とすると、式の値が print されます。 これで、電卓 program なんぞとはおさらばです。

言語の特徴は、Common Lisp に似ています。 詳しくは、elisp 用の info file を

     /usr2/home/BB/info/elisp-jp
としておいておきますので興味があれば見てみましょう。

また、mule がどこの directory にあるか見て、 その lisp program がどの様になっているか見てみるのもよいかもしれません。

Scheme
Scheme の特徴は、いろいろあるかもしれませんが、Common Lisp との大きな違いは、 static scoping である、 代入を頻繁に用いない programming style などがあげられます。

R4RS, R5RS
Scheme の仕様のひとつです。各々
The Revised^4 Report on the Algorithmic Language Scheme
The Revised^5 Report on the Algorithmic Language Scheme
の略です。

算術演算や真偽値、論理・条件式などの情報

define 関連情報

and/or 式の評価順序に関する例
and/or 式の評価の場合、引数部の評価は、 and/or 式の値が 決定するまで、つまり、各々、偽/真の値が出た時点で引数の評価が終わります。 ということで、 and, or はマクロを使わないではユーザ関数として定義出来ません。 以下の例を見てみましょう。
> (define (loop x) (loop x))    ; loop する関数を定義
> (or #t (loop 0))             ; or の評価の際は
#t                           ; 真の値が出た時点で終了
> (and #t (loop 0))            ; and の評価の際は
返って来ない                ; (loop) の評価をしようとして無限loop

> (define (user-or x y) (or x y)) ; user-or は二引数で or をするだけ
> (user-or #t (loop))        ; でも、一般関数の場合引数を全て評価しようとするので、
返って来ない                ; 無限loop に入る

(*): Question--- 本当は、式とは変数と定数を関数によって組み合わせたものではないのか?
なかなかするどいです。でも、ここでぼやかしているのは理由があります。 というのも、将来 高階関数 lambda式 のところでも説明するように、 関数それだけでも式として扱える様にする予定があります。

つまり、 * を式の構成要素としてだけではなくて、 値そのものとして使うつもりだということです。 このように値として扱えるものを first class value といい、scheme では、 function は first class value として扱われます。

その意味では、式とは変数と定数と関数とを関数によって組み合わせたもの というのがより正しいのかもしれません。

特定の評価順序
この場合の評価順序は、関数の引数部をまず評価し、 それから関数の定義にしたがった評価を行っています。つまり、まず、 引数 a, f(2, a) を評価しようとし、その評価が終わった後、 値 4, 8 に対して、 f(4, 8) を実行しようとしています。 このような評価法を値呼出し (call by value) などといいます。

これに対し、引数部の評価を後まわしにして、先に関数を展開してしまうことも出来ます。 名前呼出し (call by name) などと呼ばれます。

評価順序によっては、評価に必要な step 数が変化することもあり、 問題によっては評価順序によってプログラムが無限ループに陥ってしまうことも あります。

例えば、 loop(x) = loop(x+1), foo(x, y) = x である場合に foo(3, loop(3)) を評価する場合を考えてみましょう。 引数、つまり、 3, loop(3) をまず評価した場合、loop(3) の評価で 無限 loop に陥りますが、 foo の評価をした場合、 foo(3, loop(3)) => 3 と正常に終了します。

STEP 機能
DrScheme には step 機能というのがあるので、試してみましょう。 definition window(上部)に以下の program をいれて、 step ボタンを押してみましょう。
(define (mul x y) (* x y))
(define a 4)

(mul a (mul 2 a))
新たな window がでてきますので、next ボタンを押して行くと計算の進行状況が見えます。 色がついているところが、これからどこが評価され、 どうなるのかを示しています。

(**): 式はある値を表す
本当は、結構微妙な問題があります。 式によっては、簡約の過程が無限 loop に陥ってしまうこともあるからです。 例えば、 loop(x) = loop(x+1) である場合に、式 loop(3) の値とはなんでしょう?

より厳密には以下のように表現できます。

  • 式を簡約していき、これ以上簡約できない式の形を 正規形 (regular form) と呼びます。
  • 式が正規形を持つ場合、その正規形は一つに定まる

普通に計算が終わる場合、その式の値とは、式の正規形の事です。

変数への代入
C 言語の人に対しては、変数への破壊的代入といった方がいいかもしれません。 つまり、問題となるのは「前にあった値を壊すような代入」だからです。

これは、 C などで値を取っておくためだけの変数への代入は状態ととらえる必要が無いからです。 Scheme などでは、 let/let* を用いることで 解決しており、代入と言う形を取りません。

C と関数型プログラミング
C でプログラミングをするとき、不必要に大域変数をバンバン確保して、 更新しまくる人がいます。 このようなプログラムは、関数呼び出しの影響の範囲が分かりにくいので、 読みにくいプログラムになりがちです。

今回、関数的に書く技術を覚えておくと、 C でプログラミングする際もきれいなコードが書けるようになるかも知れません。

手続き型言語に置ける評価順序
例えば、以下のmain文でのfooの呼び出し時に、 どちらの引数を先に実行するかによって x,y の値は、 1,2 になったり 2,1 になったりしていまいます。 つまり、これによってmainの値が変わってしまいます。 これは、counter という状態の変化を どういった順番で施すかが変化してしまうからであり、 手続き型言語では評価順序が大きな意味を持ちます。

int counter = 0;
int inc_counter() { counter++;  return counter; }
int foo(int x, int y) { 
    if (y == 1) {
        return x;
    } else {
        return - x;
    }
}
int main() { return foo(inc_counter(), inc_counter()); }

関数型の利点の例
  • 評価順序を気にする必要がないので、簡単に並列・並行実行ができる。
    例えば、 (foo (expr0) (expr1)) の実行の際に、 (expr0), (expr1) の実行を二つのCPUを用いて実行することができる。 このように並列化が容易なため、 並列計算環境で高速化をおこなうことができるだろうという期待がある。
  • 特定の式の評価が他に影響を与えないので、最適化を行いやすい。
    例えば C 言語においては、
        x = 3;
        foo();
        printf("%d", x + 1);
    
    というプログラムで、 x + 1 を行った時点の x の値がよく分からないかもしれません。 例えば、 foo(); x の値を更新しているかもしれないからです。 一方、純粋に関数型の場合、そのような心配はありません。

    実は、C 言語においても x を auto local 変数としておけば、 コンパイラは x が途中で更新されていないと判断してくれます。 この場合、賢いコンパイラは、 printf("%d", x + 1); => printf("%d", 4); とあらかじめ最適化しておくこともできるのです。

関数型らしくない命令群の情報

99.10.6/ Tomio KAMADA: kamada@cs.kobe-u.ac.jp