Mukai Systems

Parenist養成ゼミ: シンボル、環境、関数

この記事は、Parenist養成ゼミシリーズの記事である。

今回は、シンボルがどのように束縛、評価されるのかを環境に着目して理解することを目的とした。また、使用例から関数の使用方法を学び、applyの仕組みが想像できるようにする。

recursionやspecial operator ifを解説していない関係上、課題の内容が不自然である。

マクローリン展開の項を増やすことによって、sinに収束している様を確認できるのは受けが良かった。

シンボル

シンボルは任意の値への参照を一つ保持でき、評価されると保持している値を返す。シンボルに値を対応付ける行為を「シンボルを束縛する」などという。

シンボルと値の対応を記録しておくために、内部的には何らかの記憶域を専有する必要がある。この記憶域のことを環境という。

環境

環境は、零個以上のシンボルと値の対応と、零または一つの外側の環境への参照をもつ。

+-------+
|E1     |
+-------+
|a:1    |
|b:2    |
+-------+
     |  +-------+
     |- |E2     |
     |  +-------+
     |  |b:20   |
     |  +-------+
     |       |  +--------+
     |       |- |E3      |
     |          +--------+
     |          |a:100   |
     |          +--------+
     |  +--------+
     |- |E4      |
     |  +--------+
     |  |a:1000  |
     |  |b:2000  |
     |  +--------+
     ...

上の図は、以下の状態を意味する。

環境E1のように、外側の環境への参照を持たない環境のことを大域環境といいシステム内にただ一つだけ存在する。

すべての環境は外側の環境をたどると大域環境に到達する。

束縛

シンボルを束縛するには、スペシャルオペレーター<-を使用する。

) (<- pi 3.14)
3.14
) pi
3.14

束縛は以下の手順で行われる。

現在の環境にシンボルが既に束縛されている場合、その値を更新する。
そうでない場合は外側の環境を再帰的に辿る。
大域環境にもシンボルが束縛されていない場合は、大域環境に新たにシンボルを束縛する。

シンボルの評価は以下の手順で行われる。

現在の環境にシンボルが既に束縛されている場合、その値を返す。
そうでない場合、再帰的に外側の環境を辿る。
大域環境にも束縛されていない場合はエラーとなる。

環境の作成

スペシャルオペレータletは現在の環境を外側の環境に持つような新たな環境を作り、その環境下で評価を行う。

(let (var1 val1 var2 val2 ...)
    expr1
    expr2
    ...)
var -- 新たに作る環境に束縛するシンボル
val -- 束縛する値
expr -- 新たな環境下で評価する式

前述した図は以下のコードによってシミュレートすることができる。

(<- a 1 b 2)
(let (b 20)
    ; この環境下では、aは1に、bは20に解決される。
    (let (a 100)
        ; この環境下では、aは100に、bは20に解決される。
        ))
; この環境下では、aは1に、bは2に解決される。
(let (a 1000 b 2000)
    ; この環境下では、aは1000に、bは2000に解決される。
    )
; この環境下では、aは1に、bは2に解決される。

関数

関数は複数の式をまとめて抽象化する仕組みである。

(function function-name (args ...)
    body ...)
function-name -- 定義する関数名
args -- 引数
body -- 実行する式

関数の変数という概念は、本質的には新たな環境を作成して、その中で評価を行うことと等しい。

例えば、以下の関数に対する呼び出しは次のような振る舞いに等しい。

(function sum3 (x y z)
    (+ x y z))

(sum3 2 4 8)

(let (x 2 y 4 z 8)
    (+ x y z))

課題

  1. 付録のファイルfun.pを作成せよ。
  2. 引数を2倍する関数_*2を実装せよ。
  3. 引数の階乗を返す関数_!を実装せよ。反復処理には付録のdotimesを使用すること。
  4. 2つの引数x, yを受け取り、xのy乗を返す関数_^を実装せよ。
  5. 引数xの正弦を返す関数_sinを実装せよ。ただし、マクローリン展開の最初の10項の和を返すものとする。
  6. 付録のファイルgen-sin-csv.pを作成せよ。
  7. paren gen-sin-csvを実行した結果をリダイレクトし、任意のグラフ描画アプリケーションに与えよ。
  8. 引数xの正弦を返す関数_sin2を実装せよ。ただし、マクローリン展開の最初のn項の和を返すものとする。
  9. gen-sin-csv.pを参考に、0~10項まで展開した結果を同時に描画できるようなcsvファイルを作成するプログラムgen-sin2-csv.pを作成せよ。数値から文字列に変換するにはstr関数を、文字列を改行せずに出力するにはwrite-bytes関数が使用できる。

実行例

$ paren fun.p
) (_*2 1)
2
) (_*2 2)
4
) (_*2 4)
8
) (_^ 1 0)
1
) (_^ 2 10)
1024
) (_! 1)
1
) (_! 2)
2
) (_! 3)
6
) (_! 4)
24
) (_! 5)
120
) (_sin 0)
0
) (_sin $pi/2)
1
) (_sin $pi)
-5.289187e-10
) (_sin $3pi/2)
-1.000003
) (_sin $2pi)
-0.001048

付録

fun.p

; symbol, environment, function

(<- $pi 3.14159265358979323846
    $pi/2 (/ $pi 2)
    $3pi/2 (* 3 $pi/2)
    $2pi (* 2 $pi))

(function _*2 (x)
  ...)

(function _! (n)
  ...)

(function _^ (x y)
  ...)

(function _sin (x)
  ...)

(function _sin2 (x n)
  ...)

(function! main (args)
  (repl))

dotimes

macro dotimesは、繰り返しを行うためのコンテキストを提供する。

dotimes自身そのものの返り値はnilであることに注意せよ。

) (dotimes (i 3)
    (write i))
0
1
2
nil
) (let (sum 0)
    (dotimes (i 3)
        (write (list :i i :sum sum))
        (<- sum (+ sum i)))
    sum)
3
) (let (sum 0)
    (dotimes (i 3)
        (<- sum (+ sum i)))
    0)
3

gen-sin-csv.p

; csv generator.

(load :fun)

(function! main (args)
  (for (i 0) (< i $2pi) (i (+ i 0.1))
    (write-line (str i "\t" (_sin i)))))