HOME もうひとつの Scheme 入門 書き込む

10. 代入


1. 初めに

今回は代入について説明します。

ここまで代入について説明しなかったのは、 代入抜きのプログラミングに慣れていだだきたかったのと、 代入にはそれなりの弊害があるからです。 代入の弊害については、 SICP: 3.1 Assignment and Local State なぜ関数プログラミングは重要か を見てください。

Scheme は基本的に関数型プログラミング言語なので、基本的には代入を用いないでプログラムを書くことができます。 しかし、代入を用いたほうがかえって簡潔に書ける場合もあり、 内部状態や継続を利用する時は代入を使う必要があります。

R5RS に定義されている代入ステートメントには set!, set-car!, set-cdr!, string-set!, vector-set! などがあります。また、そのほかに処理系依存の代入ステートメントがあります。 scheme では、代入などの破壊的なステートメントの名前にはプログラマーの注意を促すために ! が付きます。

2. set!

変数に値を代入するときに使います。Common Lisp の setf と異なり、式に値を代入することはできません。 また、set! を使う前に、変数が宣言されている必要があります。

次のように使います。

(define var 1)
(set! var (* var 10))
var ⇒ 10

(let ((i 1))
    (set! i (+ i 3))
    i)
⇒ 4

3. 代入と内部状態

3.1. 静的スコープ(レキシカルクロージャ)

Scheme の変数の有効範囲は、プログラムコードに書いてある通りです。 これを専門用語でいうと lexical closure (書いてある通りの囲い込み)または 静的スコープと呼びます。変数が、プログラムコードに書いてある通りの有効範囲を持つので、 バグが入り込む余地を少なくしています。 静的スコープに対して、関数が呼び出された環境で変数を探しに行く動的スコープもありますが、 こちらは、バグが生じやすいので、現在主流ではありません。

変数の有効範囲を限定する式には let 式、lambda 式および後で説明する letrec 式 などがあります。lambda 式の引数は、そのlambda 式内でのみ有効です。

実は、let 式は下に示すように lambda 式で置き換えることができます。

(let ((p1 v1) (p2 v2) ...) body)
⇔
((lambda (p1 p2 ...) body) v1 v2 ...)

3.2. レキシカルクロージャ と代入による内部状態の実装

レキシカルクロージャ を応用すると手続きに内部状態を持たせることができます。 たとえば、銀行口座をシミュレートする関数を書いてみましょう。 口座を作るとき 1000 円預け入れるとします。預け入れるときは正、引き出すときは負の数を引数に与えるとします。 簡単のため、預金金額が負になるのを許すことにします。(その場合は借り入れていることになります。)
(define bank-account
  (let ((amount 1000))
    (lambda (n)
      (set! amount (+ amount n))
      amount)))
残高 amount(+ amount n) を代入しています。
実行すると以下のようになります。
> (bank-account 2000)     ;2000 円預け入れ
3000

> (bank-account -2500)     ;2500 円引き出し
500

Scheme は手続きを返す手続きを書くことができるので、 銀行口座を作る関数を書くこともできます。
この例を見ると、関数型言語を使ってオブジェクト指向にするのは簡単であることがわかると思います。 実際、あとほんの少し手を加えればオブジェクト指向になります。

(define (make-bank-account amount)
  (lambda (n)
    (set! amount (+ amount n))
    amount))
> (define yamada-bank-account (make-bank-account 1000))   ; 山田さんが 1000 円預金して銀行口座を作る。
yamada-bank-account

> (yamada-bank-account 5000)                              ; 5000 円預け入れる
6000

> (yamada-bank-account -5500)                             ; 5500 円引き出す
500


> (define saito-bank-account (make-bank-account 10000))  ; 斉藤さんが 10000 円預金して銀行口座を作る。
saito-bank-account

> (saito-bank-account -7000)                             ; 7000 円引き出す
3000

> (saito-bank-account 30000)                             ; 30000 円預け入れる
33000

3.3. 副作用

Scheme の式は、括弧の外へ値を返すことを主な目的としていて、それ以外の動作を副作用と呼びます。 副作用には代入、IO などがあります。

練習問題

上の銀行口座生成関数を改良して、預金残高以上引き出そうとするとエラーになるようにして下さい。
ヒント:2つ以上の式をまとめて1つの式にするには begin 式を使います。

4. リストの破壊的操作 (set-car!, set-cdr!)

set-car!, set-cdr! はコンスセルの car 部、cdr 部に値を代入します。 set! と異なり、式に値を代入することができます。 次のように使います。
> (let ((ls  (list 1 2 3)))
    (set-car! ls 0)
    ls)
(0 2 3)

> (let ((ls (list 1 2 3)))
    (set-cdr! ls '(20 30))
    ls)
(1 20 30)

4.1. 待ち行列 (Queue) の実装

set-car!, set-cdr! を利用すると待ち行列を実装することができます。 通常のリストは先入れ後出しですが、Queue は先入れ先出しのデータです。 Queue は図1のようなデータ構造をとっており、cons-cell-top の car 部はリストに、 cons-cell-top の cdr 部はそのリストの最後のコンスセルへのポインターを持ちます。


図1:

Queue の最後に要素 (item 4) を追加するには、Queue の最後のコンスセル ((cdr cons-cell-top) で直接アクセスできます) の cdr 部のポインターを、 car 部が item 4, cdr 部が '() のコンスセルにします。 その後、cons-cell-top の cdr 部を、そのコンスセルへのポインターにします。(図 2)


図2:

一方、先頭の要素を取り出して、Queue からそれを取り除くには、先頭の要素をまず、局所変数に 保存し、その後、 cons-cell-top の car 部を リストの2番目のコンスセルに移します(図 3)。


図3:

[code 1] に R5RS 版の Queue を実装したコードを示します。 [code 1] の enqueue!queue の最後の obj を追加した Queue を返す関数、 dequeue!queue から最初の要素を取り除き、取り除いた最初の要素を返す関数です。

MzScheme では、通常の pair や list は書き換えられないので、別に書き換えられる pair, list である mutable-pair が用意されています。 通常のリストや pair が破壊的に操作できるとバグの原因になるので、両者を区別するのはいいアイデアだと思います。 だた、RnRS の仕様とは外れるので、ほかの処理系との互換性に問題が生じます。 [code 1a] に MzScheme での Queue の実装を示します。

[code 1] (RnRS 版)

(define (make-queue)
  (cons '() '()))

(define (enqueue! queue obj)
  (let ((lobj (cons obj '())))
    (if (null? (car queue))
	(begin
	  (set-car! queue lobj)
	  (set-cdr! queue lobj))
	(begin
	  (set-cdr! (cdr queue) lobj)
	  (set-cdr! queue lobj)))
    (car queue)))

(define (dequeue! queue)
  (let ((obj (car (car queue))))
    (set-car! queue (cdr (car queue)))
    obj))

[code 1a] (MzScheme 版)

(require scheme/mpair)

(define (make-queue)
  (mcons '() '()))

(define (enqueue! queue obj)
  (let ((lobj (mcons obj '())))
    (if (null? (mcar queue))
        (begin
         (set-mcar! queue lobj)
         (set-mcdr! queue lobj))
        (begin
         (set-mcdr! (mcdr queue) lobj)
         (set-mcdr! queue lobj)))
    (mcar queue)))

(define (dequeue! queue)
  (let ((obj (mcar (mcar queue))))
    (set-mcar! queue (mcdr (mcar queue)))
    obj))
> (define q (make-queue))
> (enqueue! q 'a)
{a}
> (enqueue! q 'b)
{a b}
> (enqueue! q 'c)
{a b c}
> (dequeue! q)
a
>

5. 終わりに

今回は代入について述べました。 Scheme は関数型言語なので代入を使う機会は少ないのですが、 それでも代入は時々重要な働きをします。 むやみに代入を使うと手続き型言語のコードの様になってしまいますが、 必要なときは注意して使いましょう。

また、今回は変数のスコープについても説明しました。

次回から数回は、Scheme で扱えるデータ型について解説します。

練習問題

(define (make-bank-account amount)
  (lambda (n)
    (let ((m (+ amount n)))
      (if (negative? m)
	  'error
	  (begin
	    (set! amount m)
	    amount)))))