プログラミングを頑張る日記

プログラミングを勉強して、ハッカーになろう

Common Lispを頑張る(15)

三連休最後の日です。
「実践Common Lisp」の5章、関数の説明の続きからです。

関数の戻り値

今のところ知っている関数は、最後の式を評価した値を
戻り値として返すものでした。これが一番普通の振る舞いです。
でも関数の途中で値を返すことができると便利です。
そのための特殊オペレータとして、RETORM-FROMがあるそうです。
下記は、引数よりも大きな席になる10未満の数のペアを探し、
見つかったらすぐにRETURN-FROMを使ってペアを返しています。

CL-USER> (defun foo (n)
	   (dotimes (i 10)
	     (dotimes (j 10)
	       (when (> (* i j) n)
		 (return-from foo (list i j))))))
FOO
CL-USER> (foo 8)
(1 9)
CL-USER> (foo 100)
NIL
CL-USER>

RETURN-FROMの最初の引数はそこから戻るようにしたいブロックの名前です。
この名前は評価されないのでクォートする必要なしだそう。
次の引数が戻り値ですね。
今回に関係ありませんが、dotimesが便利そうです。
変数名と数値のリストを引数として渡して、以降の式を
変数に格納する数字をインクリメントしながら実行していく感じでしょうか。

データとしての関数または高階関数

名前により呼び出すのが基本的な関数の使い方ですが、
関数をデータとして扱えると嬉しい状況もたくさんあるそうです。

Lispでは関数はオブジェクトの一種でしかありません。
関数をDEFUNで定義するとき、行われていることは2つあり、
新しい関数オブジェクトの生成と、それに名前をつけることです。
Lambda式というのを使えば、関数オブジェクトの生成のみが行われます。
関数オブジェクトの本当の表現は、名前のあるなしにかかわらず
そんなにはっきりと分かるものではなく、
知っておく必要があるのは、どうやって関数を手に入れるかということ、
手に入れた関数をどうやって起動するかということだけだそうです。

関数オブジェクトを得る仕組みはFUNCTIONという特殊オペレータが
提供してくれます。先程定義した関数FOOを得てみます。

CL-USER> (function foo)
#<FUNCTION FOO>
CL-USER>

またこのFUNCTIONは#'で書くこともできます。
QUOTEに対する'と同じ関係です。

CL-USER> #'foo
#<FUNCTION FOO>
CL-USER>

ささ、関数オブジェクトの手に入れ方はわかりました。
後は起動する方法だけです。
関数オブジェクトを通じて関数を起動する2つの関数があるそうです。

1つはFUNCALLです。これは渡す引数の個数がプログラムを書いている
時点ではっきりしている時に使うそうです。
この関数の最初の引数は起動する関数オブジェクトで、
残りの引数はその関数に渡される引数です。

CL-USER> (foo 18)
(3 7)
CL-USER> (funcall #'foo 18)
(3 7)
CL-USER>

む…これを使って何が嬉しいのかまだわかりません。
というわけで写経です。関数オブジェクトを引数として受け取り、
その引数の関数が起動された時に返す値を、これまた引数として
受け取る最小値から最大値まで指定されたstep刻みで
アスキーアートヒストグラムを描きます。

CL-USER> (defun plot (fn min max step)
	   (loop for i from min to max by step do
		(loop repeat (funcall fn i) do (format t "*"))
		(format t "~%")))
PLOT
CL-USER> (plot #'exp 0 4 1/2)
*
**
***
*****
********
*************
*********************
**********************************
*******************************************************
NIL
CL-USER>

loopの周りが難しいです…。こんな風に書けるのすごいですね。
最初のloopは…見たとおりな気がしますね。
変数iにminを最初のループで入れて、byずつ増やしmaxになるまで
doで指定した処理を回すという感じでしょうか。
次のloopは…forとrepeatで動きが違うんですかね。
repeatを最初の引数として受け取ったら次の引数の回数分do指定の
処理を実行するという感じがします。repeatという名前からしても。
で、まあ回す回数の指定は引数に渡す関数の実行結果なんですが、
その回数分"*"を出力し、"*"の出力が終わったら改行するってところですね。
loopの方に話が逸れてしまいました。
自然対数の底eのべき乗を求める組み込み関数のexpを渡すとこうなります。

CL-USER> (plot #'exp 0 4 1/2)
*
**
***
*****
********
*************
*********************
**********************************
*******************************************************
NIL
CL-USER>

ただしFUNCALLは引数リストが実行時にしかわからない場合は
いまいち使えないということです。
例えば、今のplot関数に渡したい関数、最小値、最大値、刻み値が
まとまっているリストがあるとします。
今のまま使うとしたら、こうなっちゃいます。

CL-USER> (defvar *plot-data* '(#'exp 0 4 1/2))
*PLOT-DATA*
CL-USER> (plot (car *plot-data*) (cadr *plot-data*)
	       (caddr *plot-data*) (cadddr *plot-data*))
; Evaluation aborted on #<TYPE-ERROR expected-type: (OR FUNCTION SYMBOL) datum: #'EXP>.
CL-USER> *plot-data*

あれ、動かない。むむむ。

CL-USER> (defparameter *plot-data* '(exp 0 4 1/2))
*PLOT-DATA*
CL-USER> (plot (car *plot-data*) (cadr *plot-data*)
	       (caddr *plot-data*) (cadddr *plot-data*))
*
**
***
*****
********
*************
*********************
**********************************
*******************************************************
NIL
CL-USER>

ダメ元で試してみたら動きました。
う〜ん、わからんなあ。
リストで関数を渡す時は#'する必要がないということでしょうか。

CL-USER> (funcall 'exp 4)
54.59815
CL-USER> (funcall #'exp 4)
54.59815
CL-USER> (funcall exp 4)
; Evaluation aborted on #<UNBOUND-VARIABLE EXP {100205DFF3}>.
CL-USER> (car *plot-data*)
EXP
CL-USER>

何もわからない…。"#'exp"という名前で渡しちゃった、みたいな気がしますが…。
ぐむむ…モヤモヤしますが先に進みます。

え〜と、そう、FUNCALLに引数を渡す時にリストで渡せない話でした。
そういう時はAPPLYを使えばよいということです。
使ったことあります!先程のものを書き直します。

CL-USER> (apply #'plot *plot-data*)
*
**
***
*****
********
*************
*********************
**********************************
*******************************************************
NIL
CL-USER>

さらにAPPLYの便利なところは、引数がバラバラの状態でも
最後に渡す引数がリストなら受け取ることができるという点だそうです。

を少し変えて実践してみます。

CL-USER> (defparameter *plot-data* '(0 4 1/2))
*PLOT-DATA*
CL-USER> (apply #'plot #'exp *plot-data*)
*
**
***
*****
********
*************
*********************
**********************************
*******************************************************
NIL
CL-USER>

から関数を追い出して、関数オブジェクトは別に

引数として渡す形で実行してみました。

APPLYは、適用される関数の取る引数がオプショナルなのか
レストなのかキーワード7日は気にしないということです。
ただし、ばらばらの引数と最後のリストを結合してできる引数のリストは
その関数にとって必要な引数が全て揃ったものでないといけないそうです。
そりゃそうですね。

無名関数

さて、関数を引数で受け取るような関数を書いていると、
一箇所でしか使わない、それどころか名前を呼ぶことさえない関数が
出てきたりして、そんな関数にいちいち名前をつけてあげることが
かったるくなってきます。
参考書の受け売りです。自分はまだかったるく感じていませんでした。
感じるべきだったんでしょう…。

新しい関数を作る時にDEFUNだけでなく、LAMBDA式というのが使え、
更にLAMBDA式ならば無名の関数が作れるそうです。

(lambda (parameter) body)

LAMBDA四季は、関数の動作の説明がそのまま名前になっているような
関数だと考えることもできるそうです。
だんだんイメージが掴めてきました。

とはいえ、そんな普通の関数のような使い方でLAMBDA式を使うことはないそうで、
無名関数が有用なのは、関数を引数として別の関数に渡す必要があって、
その渡す必要のある関数がインラインで書けるくらいシンプルな場合だそう。

先程までのplot関数を利用して実践です。

CL-USER> (defun double-x (x) (* x 2))
DOUBLE-X
CL-USER> (apply #'plot #'double-x *plot-data*)

*
**
***
****
*****
******
*******
********
NIL
CL-USER>

double-xという関数を定義してplotすることと、

CL-USER> (apply #'plot #'(lambda (x) (* 2 x)) *plot-data*)

*
**
***
****
*****
******
*******
********
NIL
CL-USER>

LAMBDA式を利用してこう書くことは同じということですね。

どうでもいいですが、doubleという関数を定義しようとしたら
凄い怒られたので、無名関数で良いところなら無名関数で書きたい気持ちが
湧いてきました。

Lambda式のもう1つ重要な用途は、クロージャの作成だそうです。
クロージャとは、それを作成した時点の環境の一部をキャプチャした
関数のことだそうで。…何のことやら。
それはどちらかというと変数に関わる話題だそうですので、
次の変数の章で解説してくれるそうです。ワクワク。

今日は遂にLAMBDA式の話が出てきました。
色々わからないことも増えてきましたが、諦めず頑張ります。