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

4. 関数を定義しよう


1. 初めに

前回までに、
  1. Scheme の処理系のインストールと使い方
  2. Scheme が式を評価する仕組み
  3. 基本的なリスト操作
について説明したので、今回はユーザ定義関数の作り方について説明します。 Scheme は関数型言語で、小さい関数を積み重ねてプログラムを書いていきます。 関数をうまく書けることは Scheme を使いこなす上で重要なことです。

関数の定義をするようになると処理系のコマンドラインからコードを打ち込むのは不便ですので、エディタで コードを編集してから、処理系にロードさせることになります。

2. 簡単な関数の定義とロード

シンボルに値をバインドするには define という命令を使います。 define はグローバル変数なら関数定義に限らず定義することができます。

エディタは何でもいいのですが、簡単のため、第一章でセットアップした Emacs を使うとして話を進めましょう。 Emacs を立ち上げて、 (C-x 2) または メニューの File → Split Window で Emacs のウィンドウを上下に分割します。 その後、(C-x C-f) または File → Visit New File で新しいファイルを作成します。 作成するディレクトリはたとえば "C:\doc\scheme" とし、ファイル名は "hello.scm" とします。 新しいファイルに、[code 1] のコードを打ち込んでみてください。

[code 1] (hello.scm)

; Hello world as a variable
(define vhello "Hello world")     ;1

; Hello world as a function
(define fhello (lambda ()         ;2
		 "Hello world"))
入力が終わったら (C-x C-s) または、File → Save で保存します。

次に (C-c C-l) または、メニュー の Scheme → Load Scheme File を用いて ファイルを処理系のロードします。 さて、プログラムのロードがすんだら (C-x o) で下側のウィンドウに飛び、 '> ' の後に次のように入力してください。入力が済むと 図 1 のような画面になると思います。 エディタでコードを編集 ⇔ 処理系へのロード、テスト は Scheme でプログラムを作成する基本的なサイクルです。 エディタによっては ソースコードを部分的に評価する機能を備えたものもあり、ロードする手間が省けることがあります。 (Emacs での方法を後述します。)

> vhello
"Hello world"

> fhello
#<procedure:fhello>

> (fhello)
"Hello world"

図 1: Emacs を用いて Scheme ソースコードの編集とテストを行っているところ。

define は変数を宣言する命令で、 2つの引数をとります。この命令は、最初の引数の名前の変数を宣言すると同時にその値を2番目の引数の値にします。 つまり、[code 1] の ;1 の部分は、vhello という変数を宣言し、 その値を "Hello world" にするということを意味します。

それに対し、[code 1] の ;2 の部分は、"Hello world" を返す手続き fhello を宣言しています。
手続きを定義するには lambda という特殊形式を使います。lambda は1つ以上の引数をとり、最初の引数は 定義する手続きがとる引数のリストです。この場合は引数を取らないので引数のリストは () になります。

処理系に vhello と入力するとその値 "Hello world" が返ってきます。 fhello と入力しても同様にその値 #<procedure:fhello> が返ってきます。 これは、Scheme が手続きを他のデータと同じように扱っていることを意味します。 前回説明したように、 Scheme は全てのデータをそのメモリー上のアドレスで管理しているので、メモリーにあるものは何でも一元管理できます。
fhello を手続きとして呼び出すためには (fhello) というように括弧でくくります。そうすると、 前々回で説明した手順を 踏んでこの式が評価され、その値 "Hello world" が返ってきます。

3. 引数をとる関数の定義

3.1. 決まった数の引数をとる場合

引数を取る関数を定義するときは lambda の後に引数のリストを置けば、それが、引数となります。 次のコードを farg.scm という名前で先ほど作った hello.scm があるディレクトリに保存してください。

[code 2] (farg.scm)

; hello with name
(define hello
  (lambda (name)
    (string-append "Hello " name "!")))


; sum of three numbers
(define sum3
  (lambda (a b c)
    (+ a b c)))
保存したら、(C-c C-l) などを使って scheme 処理系にロードして、関数を呼び出してみてください。
> (hello "Lucy")
"Hello Lucy!"

> (sum3 10 20 30)
60
hello
関数 hello は1つの引数 name を取り、name を "Hello " と "!" ではさんで返します。
string-append は任意個の文字列の引数をとり、それらをつなぎ合わせた文字列を 返します。
sum3
3個の数の引数をとり、それらの合計を返します。

3.2. 任意個の引数を取る方法

任意個の引数をとる関数を定義することができます。 固定された引数の数(ゼロでも化)にプラスアルファの引数(レストパラメータ)をとることができます。 プラスアルファの引数は、リストとして関数本体に渡ります。 今までの説明範囲では使い道は無いのですが、繰り返しや、高階関数 とともに利用するとエレガントなコードを書くことができます。

以下の例は、関数に渡った引数をリストにして返す関数です。 3 つの引数とレストパラメータをとります。 通常の引数の後に、ドットで区切って、レストパラメータを書きます。

(define three-args+
  (lambda (a b c . d)
    (list a b c d)))

以下は実行例です。 レストパラメータがリストとして 関数本体に渡っているのがわかると思います。

> (three-args+ 2 3 4)
(2 3 4 ())

> (three-args+ 2 3 4 5)
(2 3 4 (5))

> (three-args+ 2 3 4 5 6 7)
(2 3 4 (5 6 7))

4. 関数定義の省略形

lambda を使って関数を定義するのが正統的な関数の定義法ですが、 [code 3] に示すような 省略形が使えます。

[code 3]

; hello with name
(define (hello name)
  (string-append "Hello " name "!"))


; sum of three numbers
(define (sum3 a b c)
  (+ a b c))


; returning a list of three and additional arguments
(define (three-args+ a b c . d)
  (list a b c d))
[code 3] に示すように、呼び出されるときの形で関数を定義します。 [code 3] は [code 2] と全く同等です。この記法を嫌う人もいますが、 コードが短くなることに越したことは無いので、この解説では遠慮なく使います。

練習問題 1

次の関数を書いてください。簡単な関数ですが、しばしば使うことになると思います。
これらの関数は R5RS では定義されていませんが、便利なので、 この解説記事では今後説明抜きで使います。
  1. 引数に 1 を加えて返す関数 (1+)。
  2. 引数から 1 を引いて返す関数 (1-)。

練習問題 2

ボールを投げたときに飛ぶ距離を求める関数を以下の手順で書いてみようと思います。
  1. 角度の度を弧度法単位(ラジアン)に変換する関数。
    180 度は π ラジアンである。 π の定義は、
    (define pi (* 4 (atan 1.0)))
    を用いよ。
  2. 速度 vx で等速運動するものが t 秒間に移動する距離を求める関数。
  3. 垂直方向の初速度 vy で投げたものが落ちてくるまでの時間を 計算する関数。
    空気抵抗は無視し、重力加速度 g9.8 m s-2 とする。
    ヒント:落ちてくるときの速度は -vy になっているから、
    2 vy = g t
    が成り立つ。ここで t は落ちてくるのにかかる時間である。
  4. 1--3 の関数を利用して、初速度 v で角度 theta 度で投げたものが飛ぶ距離を求める関数。
    ヒント:まず、最初に関数を利用して角度 theta を弧度法単位に換算する(それを theta1 とする)。
    垂直、水平方向の初速度はそれぞれ v sin(theta1), v cos(theta1) で表される。 落ちてくるまでにかかる時間は関数3を用いて計算できる。 水平方向に加速度はかからないので、飛ぶ距離は関数2を用いて計算できる。
  5. 初速度 40 m s-1, 角度 30 度で投げたボールが飛ぶ距離を上で定義した関数を用いて求めよ。 (肩の強いプロ野球選手が遠投したときの距離に近い値になります。)

5. Emacs について

Emacs はプログラム作成が便利になるように設計されたエディタです。 プログラムの作成は、コードを書く作業と、テスト、修正のサイクルから成り立っています。 Emacs はこのサイクルがスムーズに回るように設計されています。

Emacs は非常にメジャーなエディタなので、書籍や web 上の情報が簡単に見つかります。 キーバインディングがほかの Window アプリと違うので、はじめ違和感があるかもしれませんが、 慣れればこんな便利なエディタはありません。 最近の Emacs はメニューバーがついているのでとっつきやすくなっています。

興味のある人は 入門 GNU Emacs を一読することをお勧めします。

Emacs の使い方

起動と終了

runemacs をダブルクリックすると Emacs が立ち上がります。 runemacs のショートカットをディスクトップに作っておくと便利です。 第1章の様に設定すると、Emacs が立ち上がったすぐ後に、処理系もついでに立ち上がります。

Emacs の終了は (C-x C-c) を使ってもいいですし、フレーム右上の X ボタンをクリックしても終了します。 そのとき、Scheme 処理系が走っているので、
"本当にとめてもいいか" 聞いてきますが、 "yes" と答えます。

ファイルのオープンと保存

(C-x, C-f) でファイルを開くことができます。存在しないファイル名を与えると新規にファイルを作成します。 初期ディレクトリは .emacs に以下のように記述して設定することができます

; C:/doc/scheme を初期ディレクトリにする
(cd "C:/doc/scheme")
(C-x C-f) と入力するとミニバッファ (フレームの下にある細長い領域) で、ファイル名を聞いてきます。 直接ファイル名を入力してもいいですし、ファイル名を忘れたときは以下のようにすることもできます。 末尾を "/" にして、ディレクトリを指定すると、メインのバッファにディレクトリのリストが表示されるので、 その中から開きたいファイルを選んで リターンするとファイルが開けます。 メニューの File → Open File でもファイルを開くことができます。 ファイルを上書き保存するときは (C-x, C-s)、 別名で保存するときは (C-x, C-w) とします。 これも、メニューからでもできますので、なれないうちはメニューからやってもいいでしょう。

インデント

C-i または TAB でインデントします。 また、改行とインデントを同時に行うには C-j を使います。

カット、コピー、およびペースト

マウスで、範囲を選択する方法もありますが、以下のようにすることもできます。
  1. まず、矢印キーまたはマウスで、選択範囲の初めにいって C-@ を押します。すると、そこが選択範囲の始点になります。
  2. 次に、 選択範囲の終点に行って、C-w を押すと、選択範囲がカットされます。 また、Alt-w を押すと 選択範囲がコピーされます。
  3. 最後に、ペーストしたい場所に行って C-y を押すとその場所にペーストされます。

領域の分割

Emacs は領域を自由に分割、統合できるので、編集作業の効率が向上します。
C-x 2
ウィンドウを上下に2つに分割します。
C-x 3
ウィンドウを左右に2つに分割します。
C-x 1
ウィンドウを1つにします。
C-x 0
カーソルのあるウィンドウを消去します。

Scheme モード特有の機能

Emacs は編集しているファイルごとに機能が切り替わります。Scheme のソースコードを編集しているときは メニューバーに Scheme という項目が現れることもそのひとつです。 Scheme モードの便利な機能として 直前の S 式の評価 (C-x C-e) があります。 これを使うと、上下のウィンドウを行ったりきたりしないで編集とテストを行うことができます。 まず、下のように、関数定義と、予想される結果を編集します。
;; 台形の面積を求める関数
; (trape 1 2 2) ==> 3
; (trape 1 1 1) ==> 1
(define (trape d1 d2 h)
  (/ (* h (+ d1 d2)) 2))
その後、関数定義の最後の括弧にあわせて (C-x C-e) を押し、 それから、テストのところに行き、閉じ括弧にあわせて (C-x C-e) を押します。 この例の場合は (trape 1 2 2)(trape 1 1 1) の閉じ括弧で (C-x C-e) を押します。 すると、図 2 のように、下の処理系のバッファに結果が表示されるので、バッファを移動しないで編集とテストを行うことができます。 テストはコメントアウトされているので、ロードやコンパイルしたときには無視されます。また、ソースコードを読むときにテストコードも一緒にあった方が 早く理解できます。

図 2: C-x C-e を使って編集とテストをしているところ

6. 終わりに

今回は関数の定義の方法について述べました。

関数定義は define を使って行います。 関数を定義するときは、処理系に直接打ち込むよりも、エディタを使ってソースコードを作ってから それをロードする方が能率的です。Emacs を使うと便利よく編集とテストができます。

次回は分岐について述べます。 分岐ができるようになると、それなりのプログラムが書けます。

練習問題の解答

練習問題 1

; 1
(define (1+ x)
  (+ x 1))

; 2
(define (1- x)
  (- x 1))

練習問題 2

以下のようなコードを書きます。
; definition of pi
(define pi (* 4 (atan 1.0)))

; degree -> radian
(define (radian deg)
  (* deg (/ pi 180.0)))

; free fall time
(define (ff-time vy)
  (/ (* 2.0 vy) 9.8))

; horizontal distance 
(define (dx vx t)
  (* vx t))

; distance
(define (distance v ang)
  (dx
   (* v (cos (radian ang)))                     ; vx
   (ff-time (* v (sin (radian ang))))))         ; t
処理系にロードした後、以下のように入力するとボールが飛ぶ距離 (m) が計算されます。
> (distance 40 30)
141.39190265868385
141.4 m と計算されます。空気抵抗を考慮に入れていないので若干大きめですが、 妥当な値が出てきました。