関数の合成

関数の切り分け方

LISP でプログラムを書き始めると、よく似た関数がいくつもできて、やはり、スパゲッティプログラムならぬヤミナベプログラムのような状態になることとは無縁ではありません。そこで全体を見渡して、共通化できるところは共通化し、それぞれの機能を考えて整理することが必要になります。

そのような事態を考えて、関数をどのように構成していけばいいか、頭においておくようお勧めするのは次の二点です。

  1. 基礎となる関数は単機能とします。単機能の関数を組み合わせて複雑な関数を組み立てていきます。逆に言いますと、一つの関数にすべてを書かないで、小さな関数に切り分けます。小さな単機能の関数とすることで、ボトムアップなデバッグが容易になります。
  2. 関数がある程度大きく機能がよく似た関数同士は、スイッチとなる引数を渡すことで機能を切り替え、一つにまとめてしまいます。このことで、よく似た関数の数が増えることを防ぎます。

関数の合成

小さな関数をより大きな関数へまとめ上げる際に、関数型言語で話題になるのが関数の合成というコンセプトです。手続き型言語で出来ないことができるというような魔法ではありませんので、もちろんそれを使わなくても最終的には同じ事ができます。しかし関数の合成は、明快さと柔軟さ、時には効率をプログラムにもたらしてくれます。

関数の合成の具体的な例は次のようなものです。

三次元の座標からなる始点と終点をもつ線分のデータが複数あるとします。

( ((Xa0 Ya0 Za0) (Xa1 Ya1 Za1))
  ((Xb0 Yb0 Zb0) (Xb1 Yb1 Zb1))
  ((Xc0 Yc0 Zc0) (Xc1 Yc1 Zc1))
  ...
)

幾何学的な計算を目的として、これらの Z 座標を切って XY 平面に投影し、始点が終点より左側、つまり始点の X 値が終点の X 値より小さくなるように必要なら始点と終点を入れ替える前処理を行っているとします。X 値が同じ場合は Y 値で始点と終点を整理します。

頂点の Z 座標を切り捨てて、XY 平面の二次元にデータにする関数は次の通りです。

(defun point:3D->2D (point)
  (list (car point) (cadr point))
)

この関数を使って、始点と終点からなる二つの三次元の頂点からなる線分を平面に投影する関数は次のようになります。

(defun line:3D->2D (line)
  (list (point:3D->2D (car line))
        (point:3D->2D (cadr line))
  )
)

次は始点と終点の順番を必要なら入れ替える関数です。X 座標で比較し、必要なら Y 座標で比較し、必要があった場合は始点と終点を入れ替えます。

(defun line:arrangeEndpoints (line / p0 p1)
  (setq p0 (car line)
        p1 (cadr line)
  )
  (cond ((equal (car p0) (car p1) 0.001)
         (if (> (cadr p0) (cadr p1))
           (list p1 p0)
           (T line)
         )
        )
        ((> (car p0) (car p1)) (list p1 p0))
        (T line)
  )
)

小さな関数が用意できましたので、より大きな関数にまとめます。今回の場合は mapcar を使うのが普通でしょう。

_$ (mapcar 'line:arrangeEndpoints
        (mapcar 'line:3D->2D
                '(((0.0 0.0 0.0) (100.0 0.0 0.0))
                  ((75.0 25.0 35.0) (50.0 25.0 100.0))
                  ((45.0 0.0 10.0) (45.0 -20.0 -10))
                 )
        )
)⏎
(((0.0 0.0) (100.0 0.0)) ((50.0 25.0) (75.0 25.0)) ((45.0 -20.0) (45.0 0.0)))

変換する関数が二つですので、mapcar も二段の入れ子になっています。小さな単機能の関数としたことで、少しプログラムがだぶついて見えます。むしろ二つの機能を一つの関数に入れた方がよかったように見えます。そこで、関数の合成を使うことによって二つの関数を一つにまとめることを考えます。

関数の合成は、一つの関数の返り値を次の関数の引数として渡すことによってパイプライン状に一度に処理を行ってしまう関数を作成することです。関数をプログラムで簡単に生成できる関数型言語の特徴を生かした考え方です。

autolisp compose

もちろん、どのような関数のペアも合成できるわけではありません。最初の関数の返り値が、次の関数の引数に正しく当てはまらなければなりません。

二つの関数を合成する関数は次のように書けます。

(defun compose (func1 func2 /)
  (eval (list 'lambda '($_item) (list (eval func2) (list (eval func1) '$_item))))
)

今回の場合は、次のように書くことで二つの変換関数を合成し、mapcar の構文も一段のシンプルなものとすることができます。

_$ (setq line:arrange (compose 'line:3D->2D 'line:arrangeEndpoints))⏎
#<USUBR @000000002bad5228 -lambda->
_$ (mapcar 'line:arrange
        '(((0.0 0.0 0.0) (100.0 0.0 0.0))
          ((75.0 25.0 35.0) (50.0 25.0 100.0))
          ((45.0 0.0 10.0) (45.0 -20.0 -10))
         )
)⏎
(((0.0 0.0) (100.0 0.0)) ((50.0 25.0) (75.0 25.0)) ((45.0 -20.0) (45.0 0.0)))

compose 関数で合成され、シンボル line:arrange に代入された関数は、下のように書いたものと実質同じものとなっています。

(defun line:arrange (line)
	  (line:arrangeEndpoints (line:3D->2D line))
)

以上、見てきたようなことが関数の合成です。小さな関数でも、秩序的により大きな関数へと発展できることがわかります。

関数名、関数、関数の実行

LISP では、関数の実行は次のように関数名と引数をカッコで囲って書きます。

_$ (expt 2 3)⏎
8

一方で、匿名の関数を使った次のような式も実行できました。

_$ ((lambda (number) (* number number number)) 2)⏎
8

匿名の関数を生成する lambda 関数は関数そのものを返しますので、上の例はつまり次のような式を実行しているのと同じです。

(#<USUBR @000000002bbd39a8 -lambda-> 2)

ここから見えてくるのは、最初の例も実は次のような式の評価なのだとわかります。

(#<SUBR @000000002cd32a68 EXPT> 2 3)

関数名、すなわち関数が代入されているシンボル名は、関数の仮の住処のようなもので別のシンボルに代入されれば、そのシンボル名でその関数が使えるようになりますし、おなじみの関数名でも別の関数をあえて代入してしまえば別の関数に変わってしまいます。

関数の合成を行う compose 関数では、引数で与えられた関数名を合成時に eval 関数で評価することによって関数そのものを合成関数に埋め込むようにしています。

もう一つの関数の合成

先に取り上げたのが関数型言語の本などで紹介されている関数の合成の考え方ですが、ここでは別のタイプの関数の合成を考えてみます。別のタイプとは、apply 関数のような引数に関数名とその関数用の引数をとるタイプです。先の例がパイプラインに例えられる形をしていましたが、今度は関数が入れ子になるようなイメージに関数を合成します。

autolisp compose

合成する関数は、引数が関数名とその関数の引数のものに限られますが、そのような二つの関数を合成する関数は次のように書けます。

(defun compose-apply (applyFunc1 applyFunc2 /)
  (eval (list 'lambda
              '($_func $_args)
              (list (eval applyFunc1)
                    (list 'quote (list 'quote (eval applyFunc2)))
                    '(list $_func $_args)
              )
        )
  )
)

apply 型の関数の合成の応用例を次に見ていきます。

AutoCAD のコマンド用の関数はシステム変数を一時的に退避させた上で実行するような形式となります。エラーが発生した場合のエラー関数の使用も定石です。

(defun star:error (msg)
  (if command-s
    (progn (command-s "._undo" "_e") (command-s "._U"))
    (progn (command "._undo" "_e") (command "._U"))
  )
  (setvar "CMDECHO" CMDECHO)
  (setvar "BLIPMODE" BLIPMODE)
  (setvar "OSMODE" OSMODE)
  (princ)
)

(defun c:star (/ radius center CMDECHO BLIPMODE OSMODE *error*)
  (if (and (setq center (progn (initget (+ 1 2 4) "")
                               (exception 'getpoint (list "\n中心を指定 : ") nil)
                        )
           )
           (setq radius (progn (initget (+ 1 2 4 64) "")
                               (exception 'getdist (list center "\n半径を入力 : ") nil)
                        )
           )
      )
    (progn (setq CMDECHO  (getvar "CMDECHO")
                 BLIPMODE (getvar "BLIPMODE")
                 OSMODE   (getvar "OSMODE")
                 *error*  star:error
           )
           (setvar "CMDECHO" 0)
           (setvar "BLIPMODE" 0)
           (setvar "OSMODE" 0)
           (command "._undo" "_be")
           ;;
           (drawPolyline (StarData center radius) T)
           (command "._HATCH" "_S" "_S" (entlast) "")
           ;;
           (command "._undo" "_e")
           (setvar "CMDECHO" CMDECHO)
           (setvar "BLIPMODE" BLIPMODE)
           (setvar "OSMODE" OSMODE)
    )
  )
  (princ)
)

この毎回繰り返される煩雑さを、関数の合成で抽象化しシンプルに書けるように考えます。

次のような、apply 関数のスタイルで引数を受け取り、実際の処理はシステム変数を退避させ、引数の関数を実行し、またシステム変数をもとに戻す関数を用意します。最後の undo-group 関数は、引数の関数の実行を UNDO コマンドのグループ化を行って実行します。UNDO コマンドを使用するため、システム変数 CMDECHO のオフもこの関数で行っています。

(defun blipmode-off ($_func $_args / $_blipmode $_result)
  (setq $_blipmode (getvar "BLIPMODE"))
  (setvar "BLIPMODE" 0)
  (setq $_result (apply $_func $_args))
  (setvar "BLIPMODE" $_blipmode)
  $_result
)

(defun osmode-off ($_func $_args / $_osmode $_result)
  (setq $_osmode (getvar "OSMODE"))
  (setvar "OSMODE" 0)
  (setq $_result (apply $_func $_args))
  (setvar "OSMODE" $_osmode)
  $_result
)

(defun undo-group ($_func $_args / $_cmdecho $_result)
  (setq $_cmdecho (getvar "CMDECHO"))
  (setvar "CMDECHO" 0)
  (command "._undo" "_be")
  (setq $_result (apply $_func $_args))
  (command "._undo" "_e")
  (setvar "CMDECHO" $_cmdecho)
  $_result
)

ここで、blipmode-off 関数と osmode-off 関数を合成すれば、システム変数の BLIPMODE と OSMODE がオフの状態で関数が実行される apply 関数と同じ使い勝手の関数が生成できます。さらに別のシステム変数を退避させたいとすれば、さらにその関数を合成してやればよいわけです。

先ほどのコマンド関数のシステム変数を退避させ元に戻す処理は、次のように外部化しシンプルに書くことができるようになります。シンボル sysValue-off に三つの関数を合成した関数を代入し、apply 型の関数として使用しています。

(defun c:star (/ radius center sysValue-off)
  (if (and (setq center (progn (initget (+ 1 2 4) "")
                               (exception 'getpoint (list "\n中心を指定 : ") nil)
                        )
           )
           (setq radius (progn (initget (+ 1 2 4 64) "")
                               (exception 'getdist (list center "\n半径を入力 : ") nil)
                        )
           )
      )
    (progn (setq sysValue-off (compose-apply (compose-apply
                                               'undo-group
                                               'osmode-off
                                             )
                                             'blipmode-off
                              )
           )
           (sysValue-off
             (function (lambda (c r)
                         (drawPolyline (StarData c r) T)
                         (command "._HATCH" "_S" "_S" (entlast) "")
                       )
             )
             (list center radius)
           )
    )
  )
  (princ)
)

compose-apply 関数で合成され、シンボル sysValue-off に代入された関数は、下のように書いたものと実質同じものとなっています。

(defun sysValue-off  ($_func $_args)
	  (undo-group 'osmode-off (list 'blipmode-off (list $_func $_args)))
)

まだ完成ではありません。エラーが発生した場合にシステム変数を元に戻す処理を、エラー関数で対応しておかなければなりません。関数が合成される必要がなければ、例えば次のように書けるでしょう。

(defun blipmode-off ($_func $_args / $_blipmode $_result *error*)
  (setq $_blipmode (getvar "BLIPMODE")
        *error*  (lambda (msg) (setvar "BLIPMODE" $_blipmode))
  )
  (setvar "BLIPMODE" 0)
  (setq $_result (apply $_func $_args))
  (setvar "BLIPMODE" $_blipmode)
  $_result
)

しかし、関数がどのような組み合わせで合成されても、エラー関数が対応できるようにするには、エラー関数も合成するようにします。合成のタイプはパイプライン型の関数の合成で、現在のエラー関数から以前のエラー関数へ処理が連続するようにします。また、エラー関数を表すシンボル *error* は、合成前は以前のエラー関数を、合成後は現在のエラー関数を表さなければなりませんので、匿名の関数への引数としてエラー関数を渡すというトリックを用います。

エラー関数に対応した合成用の関数は次のようになります。これらを使用しているコマンド側の関数の方は変更の必要はありません。

(defun blipmode-off ($_func $_args / $_blipmode $_result)
  (setq $_blipmode (getvar "BLIPMODE"))
  ((lambda (*error*)
     (setvar "BLIPMODE" 0)
     (setq $_result (apply $_func $_args))
     (setvar "BLIPMODE" $_blipmode)
     $_result
   )
    (compose (function (lambda (msg) (setvar "BLIPMODE" $_blipmode) msg)) '*error*)
  )
)

(defun osmode-off ($_func $_args / $_osmode $_result)
  (setq $_osmode (getvar "OSMODE"))
  ((lambda (*error*)
     (setvar "OSMODE" 0)
     (setq $_result (apply $_func $_args))
     (setvar "OSMODE" $_osmode)
     $_result
   )
    (compose (function (lambda (msg) (setvar "OSMODE" $_osmode) msg)) '*error*)
  )
)

(defun undo-group ($_func $_args / $_cmdecho $_result)
  (setq $_cmdecho (getvar "CMDECHO"))
  ((lambda (*error*)
     (setvar "CMDECHO" 0)
     (command "._undo" "_be")
     (setq $_result (apply $_func $_args))
     (command "._undo" "_e")
     (setvar "CMDECHO" $_cmdecho)
     $_result
   )
    (compose (function (lambda (msg)
                         (if command-s
                           (progn (command-s "._undo" "_e") (command-s "._U"))
                           (progn (command "._undo" "_e") (command "._U"))
                         )
                         (setvar "CMDECHO" $_cmdecho)
                         msg
                       )
             )
             '*error*
    )
  )
)

今回の例では全体的に書いたコードの量は増えましたが、これらの合成用の関数は必要に応じて繰り返し使えること、これらの関数が行っていることについては今後特に知らなくてもよくプログラミングのメインテーマに専念できることを考えると、少しの手間で便利な道具が少しずつ増えていくようなものです。

以上のように関数型言語の考え方を導入することで、プログラムの姿が大きく変わったことが見て取れると思います。

以降余談になりますが、どのようなコマンドも処理の始めと終わりに UNDO コマンドのグループ化を行った方が使い勝手の良いコマンドとなります。上の例では、UNDOコマンドの操作とシステム変数 CMDECHO を管理する関数が一つになっていました。ちなみに ActiveX 関数を使えば【コマンドライン】に UNDO コマンドを表示させることなく実行することが可能になり、二つの機能は別々の関数に分けることができます。今回はこのままとして、UNDO のグループ化を行うと CMDECHO もオフになりますので、グループ化された手続きの中で場合によってはあえて【コマンドライン】にプロンプトを表示させたい場合に備えて、一時的に CMDECHO をオンにする次のような関数を用意しておくと便利です。

(defun echo ($_func $_args / $_cmdecho $_result)
  (setq $_cmdecho (getvar "CMDECHO"))
  (setvar "CMDECHO" 1)
  (setq $_result (apply $_func $_args))
  (setvar "CMDECHO" $_cmdecho)
  $_result
)