関数

関数の定義

AutoLISP関数
(defun symbol ([arguments] [/ variables...]) expr...)
symbol:シンボル
arguments:シンボル
variables:シンボル
expr:式
関数を定義します。
戻り値:シンボル

defun 関数は、「関数を定義」する関数で、symbol 引数で指定したシンボル名の関数を定義します。defun 関数は特殊形式として、他の式、他の関数の「内側優先、左側優先」の評価の法則とは異なる書式をとっています。

特殊形式

「(defun symbol ([arguments] [/ variables...]) expr...)」の「([arguments] [/ variables...])」はリストでしょうか式でしょうか?「expr...」の部分も、本来の関数の引数ならば「内側優先、左側優先」の法則で defun 関数が実行される前に評価されるはずですが?

これらは、特殊形式として LISP の文法や作法から外れています。特殊形式は、すっきりとコードを書くことができるようにの基本関数内ではよく見られるものです。

関数が受け取る引数は arguments の部分に半角スペースで区切って並べます。

省略可能な引数

システム内蔵の関数の中には、省略可能な引数を持つ関数が多く見られます。しかし、AutoLISP でユーザーが定義する関数では、引数を省略可能と定義することはできません。状況により数が変わるような引数を受け取る関数を定義したい場合は、引数をリストに格納して、関数にはリストを渡すようにします。

続いて、関数内で使用するローカル変数を arguments 引数の後に「/」(スラッシュ)で区切って variables に一括して定義します。/ の前後には、必ず半角スペースでシンボルと「/」を離してください。ローカル変数は nil で初期化されます。

arguments と variables は関数内でローカルな変数として扱われ、呼び出し元の変数に影響を与えません。AutoLISP には一時的にローカルな変数を宣言する Common Lisp の let 関数のようなものがありません。ローカルな変数は、AutoLISP では関数宣言のところですべて行わなければなりません。

予約語

AutoLISP には一般のプログラミング言語で言うところの予約語というものはありません。quote も eval もシンボルとして他のと同等です。あえて予約語のようなものとして挙げられるのは、リストを表す二つのカッコ「(」「)」や文字列を囲む「"」、quote を表す「'」です。これらは他のシンボルと半角スペースで区切られていなくても特別な記号として有効に作用します。そして、その特殊性ゆえにシンボルの名前に使えない文字です。

一方、関数定義での「/」やドットリストの「.」は特別扱いされておらず、シンボル名の一部で使える文字でもあります。また「/」はこの一文字では除算を表す関数のシンボル名でもあります。そのため、これらの記号を関数定義やドットリストで使う時は、必ず半角スペースで他のシンボルと区切らなければ正しく解釈されません。

expr.. の部分に関数の内容を記述します。defun のカッコ内であれば「暗黙の progn」のために progn を用いることなく複数の式を並べることができます。

defun 関数自体の戻り値は関数名、すなわち symbol 引数で指定したものとおなじものです。一方、定義された関数を実行した時の戻り値は、expr.. の部分で最後に評価された式の値となりますので、他のプログラミング言語で明示的に返す値を指定するのとは異なり、評価の順番に配慮する必要があります。

関数定義の具体例を示します。

以下は線形合同法による擬似乱数を返す関数の例です。関数の引数はありませんが、グローバル変数の *randamSeed* を参照しています。

(setq *randamSeed* 1)

(defun irand () (setq *randamSeed* (1+ (* *randamSeed* 69069))))

この関数を続けて呼び出すとランダムな整数が得られます。

_$ (repeat 5 (print (irand)) )⏎

69070
475628535
-1017563188
772999773
-417135238 -417135238

グローバル変数のシンボル名

LISP ではグローバル変数名を「*foo*」のように前後を*(アスタリスク)で囲むのが一つの慣例です。名前の付け方は強制ではありませんが、ルールを統一した方がわかりやすいプログラムになります。

次は三角関数の tan を求める関数です。「radian」は関数の引数です。

(defun tan (radian) (/ (sin radian) (cos radian)))

実行すると以下のようになります。

_$ (tan (/ pi 4)) ⏎
1.0
_$ (tan (/ pi 3)) ⏎
1.73205

次の例は、整数を二進数を表す文字列に変換します。引数定義の「/」以降の「i blist mask」はローカル変数として宣言されています。

(defun itob (a / i blist mask)
  (setq blist ""
        mask 1
        i 1
  )
  (repeat 32
    (if (and (/= i 1) (= (rem i 8) 1))
      (setq blist (strcat ":" blist))
    )
    (if (zerop (logand a mask))
      (setq blist (strcat "0" blist))
      (setq blist (strcat "1" blist))
    )
    (setq mask (lsh mask 1)
          i    (1+ i)
    )
  )
  blist
)

実行すると以下のようになります。

_$ (itob 1)
"00000000:00000000:00000000:00000001"
_$ (itob 255)
"00000000:00000000:00000000:11111111"
_$ (itob -1)
"11111111:11111111:11111111:11111111"

再帰関数も、もちろん定義できます。次の関数はフィボナッチ数列の n 番目の項を返す関数です。

(defun fibonacci (n /)
  (if (and (= (type n) 'INT) (<= 0 n))
    (cond ((= n 0) 0)
          ((= n 1) 1)
          (T (+ (fibonacci (- n 1)) (fibonacci (- n 2))))
    )
  )
)

実行すると以下のようになります。

_$ (fibonacci 1)
1
_$ (fibonacci 2)
1
_$ (fibonacci 3)
2
_$ (fibonacci 4)
3
_$ (fibonacci 5)
5

変数のスコープ

AutoLISP の変数のスコープは、最近の他の言語と最も考え方が異なる部分です。Common Lisp も最近の言語に入ります。Common Lisp で AutoLISP と同じような変数のスコープとするためには、変数を特別にそうであると宣言しなければなりません。

AutoLISP の変数のスコープの考え方は、関数内である変数にアクセスしようとした時、関数内で使用している変数でローカル変数と宣言されているもの以外は、関数の外にある変数にアクセスしようとします。関数の外とは、必ずしもいわゆるグローバル変数を指しません。関数の外とは、まず呼び出した元の関数を当たります。そして呼び出し元の関数に変数が無ければさらに外の関数のものにアクセスします。そして最終的に見つからなければ、いわゆるグローバル変数の中から参照します。つまりローカル変数の宣言の状況にとっては、呼び出された関数から呼び出し元の関数のローカル変数にアクセスでき、自由に読み書きすることが出来るのです。このような仕組みをダイナミックスコープ(dynamic scope) と言います。

この仕組みを別に言い換えますと、関数が呼び出されてローカル変数が確保された場合、同じ名前の変数がグローバル変数や呼び出し元の関数にあった場合、それら既存の変数はマスクされ保護されます。新しいローカル変数と被らなかった名前の変数はマスクされませんので、呼び出された関数からでもアクセス可能です。

ダイナミックスコープ

なお、上の図で例えば変数 A などといった、どこにも使われていない変数の値を参照しようとした場合は、未定義と言う意味での「nil」という値が返ります。

ダイナミックスコープの簡単な実例を以下に示します。

(defun boo ()                           ;呼び出される関数
  (setq fooVal "BOO!")
)

(defun foo (/ fooVal)                   ;呼び出し元の関数、ローカル変数 fooVal
  (setq fooVal "FOO!")
  (boo)                                 ;関数 boo の呼出し
  fooVal                                ;fooVal の値を返す
)

実行してみると、呼び出された側の関数で書き換えた値が返ります。

_$ (foo)⏎
"BOO!"

これは、思いがけないところに副作用を引き起こす、大変危険な仕組みです。関数型言語の理念に限らず、呼び出された関数側で使いたい値は引数で受渡しを行い、そして、誤って外の変数を書き換えないように関数内で使う変数名はローカル変数としていちいち宣言するのが、よい習慣です。

しかし、実際のプログラミングでは、この仕組みを便利に使うこともできます。プログラミングしているうちに以下のように関数が巨大になる場合があります。

(defun とても複雑なことをする関数 (引数 / とても多くのローカル変数)
    (とても多くのローカル変数の初期化)
    (とても複雑なことをする式の評価―ステップ1)
    (とても複雑なことをする式の評価―ステップ2)
)

関数が一画面内でおさまらなくなり、カッコの対応も取りにくく、可読性も落ちます。このような関数を以下のように分解します。

(defun とても複雑なことをする関数:ステップ1 (/)
    (とても複雑なことをする式の評価―ステップ1)
)

(defun とても複雑なことをする関数:ステップ2 (/)
    (とても複雑なことをする式の評価―ステップ2)
)

(defun とても複雑なことをする関数 (引数 / とても多くのローカル変数)
    (とても多くのローカル変数の初期化)
    (とても複雑なことをする関数:ステップ1 の呼出し)
    (とても複雑なことをする関数:ステップ2 の呼出し)
)

最初の二つのサブ関数には、引数もローカル変数の宣言もありません。しかしサブ関数からは、よび出し元のメイン関数のローカル変数に、引数を間違いなく受け取る困難も無く、分解前と同じようにアクセスできます。そして、最後のメイン関数はシンプルな構造になり、サブ関数の名前の付け方を工夫することによって可読性を高めることができます。デメリットは、サブ関数がメイン関数から呼び出されることを前提としているので、他の関係ないところからサブ関数が呼び出される危険性があることですが、サブ関数の名前をメイン関数の名前と関係したものにすることで、危険性がほとんど無い状態にすることができます。

このようなメイン関数とサブ関数の構成にすると、ローカル変数の中でもサブ関数でしか使用しないものもあるでしょう。メイン関数で多くのローカル関数を宣言するより、適時サブ関数で宣言を行ったほうが解りやすくなります。

(defun とても複雑なことをする関数:ステップ1 (/ サブ関数のローカル変数)
    (サブ関数のローカル変数の初期化)
    (とても複雑なことをする式の評価―ステップ1)
)

(defun とても複雑なことをする関数:ステップ2 (/ サブ関数のローカル変数)
    (サブ関数のローカル変数の初期化)
    (とても複雑なことをする式の評価―ステップ2)
)

(defun とても複雑なことをする関数 (引数 / 共通して使うローカル変数)
    (共通して使うローカル変数の初期化)
    (とても複雑なことをする関数:ステップ1 の呼出し)
    (とても複雑なことをする関数:ステップ2 の呼出し)
)

このように整理した場合、サブ関数は Common LISP の let 構文と同じ効果を持ちます。むしろメイン関数の中で書いた let 構文よりサブ関数で分離したほうが、処理のステップが簡潔に表現され可読性も増します。ダイナミックスコープの仕組みを使わずにサブ関数に引数を渡すようにして、このサブ関数が何の値を必要としているかを明確化してもいいでしょう。

メイン関数サブ関数の明確な階層構造がない場合でも幾つかの関数でまたいで使う変数と値があった場合、グローバル変数にすることをまず考えますが、よくある単語では他で使用している危険性が出てきます。このような場合、その状況の主となる関数のローカル変数にしておけば、外側のグローバル変数に影響を与えることなく、補となる関数から自由にアクセスできます。

ダイナミックスコープという仕組みや、このように外の変数を参照する前提で関数を分解して記述するスタイルは、最近のプログラミング言語の傾向や関数型言語の大事な理念「参照透明性」と反するもので、大手を振ってお勧めすることは出来ないものです。関数型言語の教義を強く大事にする人の中には、関数にはすべての値を引数で渡すのが原則だと主張する方もいるでしょう。しかしながら、これら整理の過程の各段階はどれも正解であり、サブ関数の切り出し方など様々な書き方があります。関数の一般性、可搬性、再利用性といったものと、それに対する特殊性や可読性を吟味して、プログラムを設計してください。