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

4. 関数を定義しよう


1. 初めに

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

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

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

グローバル変数 を宣言し、それに値を結びつけるには define という命令を使います。 define は通常の変数の宣言の他に、関数を定義するためにも使われます。 (Scheme には関数と通常の変数の区別がないことを思い出してください。)

ここでは、第一章でセットアップした Emacs での作業を例にとって話を進めます。DrScheme を用いた場合も 基本的にはほとんど同じです。 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 の場合はA-6: 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)))
保存したら、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

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

練習問題 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. 終わりに

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

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

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

練習問題の解答

練習問題 1

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

; 2
(define (dec 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 と計算されます。空気抵抗を考慮に入れていないので若干大きめですが、 妥当な値が出てきました。