部分適用

部分適用

今回は、関数型言語の話題としてあげられる関数の引数の部分適用について紹介します。複数の引数を受け付ける関数があったとします。一度に引数のセットを渡して関数から値を得るのが普通ですが、部分適用は引数を一部分しか渡しません。当然関数から最終的な値は得られませんが、「部分的に引数が適用された関数(の状態)」が得られます。そして別の機会にそれに残りの引数を与えたときに、元々得るはずであった値が確定します。初めて聞くときは不思議な話だと思われるでしょう。

部分適用とはどういうものかという実例を示します。AutoLISP 関数で単位変換を行う cvunit を例で用います。cvunit 関数は以下のような書式で、指示された単位にしたがって数値を変換して返します。

(cvunit value from-unit to-unit)

通常の使い方は次のようなものです。

_$ (cvunit 3600 "mm" "m")⏎
3.6

AutoLISP の場合の部分適用は、次のようなカスタム関数を新しく定義することにあたります。引数 from-unit と to-unit が部分適用されています。

(defun mm->m (value) (cvunit value "mm" "m"))

なぜこのようなことをするのでしょうか。機能が限定されたカスタム関数を使用することによって、プログラムを読み書きし易く誤りをなくす以外に、この関数を他の関数と合成することも容易になるからです。関数の合成は、関数型言語の強力な手段です。

カリー化

部分適用とよく似た話題で関数のカリー化というものがあります。関数のカリー化のイメージを示すと以下のようなものになります。AutoLISP には無いクロージャーの仕組みを使っているため、AutoLISP では動きません。

(setq cvunit-curry (lambda (from-unit)
                      (lambda (to-unit)
                        (lambda (value) (cvunit value from-unit to-unit))
                      )
                    )
)

ここで生成された関数 cvunit-curry を呼び出すためには「(((cvunit-curry "mm") "m") 3600) 」と書かれます。

カリー化は、複数の引数をもつ関数の呼び出しも一つずつの引数渡しの関数の形に書き表せるという「理論」です。「((cvunit-curry "mm") "m")」の部分が評価されると、先ほどの部分適用で見た mm->m 関数と同じ関数となります。実際にカリー化を行えるプログラミング言語では、部分適用と同じく引数を減らした関数を生成することで関数の合成などを行うテクニックなどとして用いられます。

カリー

カリー化とは、論理学者のハスケル・カリー氏にちなんでこう呼ばれます。

部分適用した関数を返す関数

部分適用は引数の一部を適用されたカスタム関数を定義することだと述べました。defun 関数を使ってそのカスタム関数を実際に書き下しても良いのですが、プログラム内でカスタム関数を自由に生成することができれば、プログラミングの柔軟性が増します。ここでは、部分適用した関数を返す関数を作ってみます。

(defun apply-partial (func partialArgs slots / arrangeArgs)
  (defun arrangeArgs (counter alist slist)
    (if slist
      (if (= counter (car slist))
        (append (list (read (strcat "$_" (itoa counter))))
                (arrangeArgs (1+ counter) alist (cdr slist))
        )
        (if alist
          (append (list (car alist)) (arrangeArgs (1+ counter) (cdr alist) slist))
          (exit)
        )
      )
      alist
    )
  )
  (eval (list 'lambda
              (mapcar (function (lambda (slot) (read (strcat "$_" (itoa slot))))) slots)
              (append (list (eval func))
                      (arrangeArgs
                        1
                        (mapcar (function (lambda (arg) (list 'quote arg))) partialArgs)
                        slots
                      )
              )
        )
  )
)

apply-partial が目的の関数の名前です。内部に arrangeArgs というローカル関数が定義されています。後半の eval 関数の中身が本体です。

apply-partial 関数の使い方は、引数 func に適用する関数名、partialArgs 引数に今回適用する引数の値を適用しない引数は飛ばしながら順にリストの形で並べます。slots 引数には今回適用されない引数の番号を整数のリストで指定します。順番の数え方は、第一引数が 1 です。

_$ (setq mm->m (apply-partial 'cvunit '("mm" "m") '(1)))⏎

上のように実行すると以下のようなリストが生成され、eval 関数で関数に変換されてシンボル mm->m に代入されます。

(LAMBDA ($_1) (#<SUBR @0000000036af7d40 CVUNIT> $_1 (QUOTE "mm") (QUOTE "m")))
_$ (mm->m 3600)⏎
3.6

適用する引数の数や値を変えれば、自在にカスタム関数ができます。

_$ (setq mm-> (apply-partial 'cvunit '("mm") '(1 3)))⏎
#<USUBR @000000003b5a19f8 -lambda->

上の場合は、次のようなリストが生成されて関数に変換されます。

(LAMBDA ($_1 $_3) (#<SUBR @0000000035667d40 CVUNIT> $_1 (QUOTE "mm") $_3))
_$ (mm-> 3600 "m")⏎
3.6
_$ (mm-> 3600 "cm")⏎
360.0

もちろん cvunit 関数以外でも使えます。次の例は値が 10 より大きい場合は 10 に丸める関数です.

_$ (setq trim:10 (apply-partial 'min '(10) '(1)))
#<USUBR @000000003b4624f8 -lambda->

この場合は、次のようなリストが生成されて関数に変換されています。

(LAMBDA ($_1) (#<SUBR @0000000036af8ae8 MIN> $_1 (QUOTE 10)))
_$ (trim:10 12)⏎
10
_$ (trim:10 9)⏎
9 _$ (trim:10 10)⏎
10

このように部分適用を行うことによって、元々が複数の引数を受け付ける関数であっても他の関数と合成することも可能になりました。次の例は、関数を合成する compose 関数を使って、単位 mm の数値のリストを単位 m に変換して 10 より大きい値は 10 に丸めるという処理を行っています。

_$ (mapcar '(compose 'mm->m 'trim:10) '(12385 8125 9057 25384 180))
(10.0 8.125 9.057 10.0 0.18)

手続き型言語が、命令を順番に書き並べておいた静的なコードを順に実行していくイメージに対して、関数型言語の場合は自在に関数というピースを組み替えて実行を行う、しかもその組み替えはコードを書いている時だけではなく、プログラムを実行しているときにも行えるというイメージを持っていただけたらと思います。