Lispの機能の部分的な適用とカレー

他のプログラミング言語に対するLispのよく知られた利点の1つは、Lispの他のどこよりも、最新のプログラミング言語に現れるさまざまな新しいメカニズムを実装するのが簡単なことです。これにはいくつかの理由があります。これは、Lispのホモイコニシティ(プログラムとデータの表現の統一された形式)と、その機能がユニークなマクロシステムです。一般に、Lispは「プログラム可能なプログラミング言語」と呼ばれる理由があります。



この短いメモでは、部分的なアプリケーションや機能のカレーなど、現在非常に人気のあるソフトウェアメカニズムをLispに実装する方法を見ていきます。その際、Homelisp実装を使用します(これは、いくつかの追加機能を備えた純粋なLispインタープリターです)。



Common Lispで部分的なアプリケーションを使用するのは少し注意が必要です(計算された関数オブジェクトを呼び出すにはfuncall / applyを使用する必要があるため)。スキームでは、それはより簡単なはずです。計算された関数オブジェクトを呼び出すためにfuncall / applyを必要としない、HomeLispの新しいバージョンをすぐに投稿する予定です。コードの動作が異なる場合は、これを強調します。



部分的な塗布とカレーは厳密な数学的操作です。しかし、ラムダ計算や組み合わせ論理に頼ることなく、それらを「指で」検討します。



関数fの部分的なアプリケーションは、関数fから、指定された引数をすでに取得し、残りを受け入れる準備ができている新しい関数f 'を取得することです。部分申請とは何ですか?たとえば、関数から関数値を返すことができるようにします。



簡単な例で部分的なアプリケーションを考えてみましょう。関数fが次の式で与えられるとします:



f(x、y、z)= x + y ** 2 + z ** 3



次に、引数x = 1およびy = 2でこの関数を部分的に適用すると、関数



f '(z)=が生成されます。1 + 4 + z ** 3 = 5 + z ** 3



Haskellでは、部分的なアプリケーションはプログラマーに何の費用もかかりません。



Prelude> f x y z = x+y**2+z**3  --  
f :: Floating a => a -> a -> a -> a
Prelude> f 1 2 3 -- ...
32.0 -- !
it :: Floating a => a
Prelude> f' = f 1 2  --     x=1 y=2     f'
f' :: Floating a => a -> a
Prelude> f' 3 --  f'   
32.0 --    
it :: Floating a => a


ただし、Lispではこの試みは失敗します。



(defun f (x y z) (+ x (* y y) (* z z z))) ;;  
==> F

(f 1 2 3) ;;  
==> 32 ;; 

(f 1 2) ;;     ...

PairLis:     

: F
  : (X Y Z)
 : (1 2)
==> ERRSTATE


もちろん、Lispには、任意の数の引数を持つ関数を作成するためのメカニズムがあります(&rest構文)。 2つまたは3つ(または10)のパラメーターを受け取る関数を作成できます。



(defun s (&rest x) ;;  -     x
  (apply '+ x)) ;;    
==> S

(s 1 2) ;;  
==> 3

(s 1 2 3) ;;  
==> 6


ただし、違いを理解することが重要です。この関数はすべてのパラメーターを処理して計算結果を返しますが、部分的に適用すると、「計算を続行する準備ができた」新しい関数が生成されます。



Lispで部分関数アプリケーションメカニズムを実装する方法を見てみましょう。そしてそれはこれで私たちを助けます...はい、匿名機能の装置(ラムダ)。一部のプログラマーは、匿名関数は名前を保存するためだけに必要であると考えています(たとえば、シーケンス要素に対して短いアクションを実行するために、stream-map-filter-reduceに配置します)。実際、匿名関数にはさらに多くの機能があります。



まず、関数が結果として別の関数を返す方法を見てみましょう。 Lispでは、それは非常に簡単です。



(defun func-gen (x)
   (lambda (y) (+ x (* y y))))
==> FUNC-GEN

(func-gen 5)
==> (CLOSURE (Y) ((+ X (* Y Y))) ((X 5)))


ご覧のとおり、この関数は、自由変数xの値が固定されている(5に等しい)クロージャーを返します。関数の結果は、関数として呼び出すことができます。これを行うには、Common LispおよびHomeLisp(カーネルリビジョン<= 13.53)で、funcallを使用する必要があります。



(funcall (func-gen 5) 7)
==> 54


これで、n個の引数とxの1つの値から関数fを取得し、その結果(n-1)個の引数から関数を取得する場合にどのように進めることができるかが明確になりました。関数の正式なパラメータのリストをplistとして示しましょう。次に、次のようなラムダ式を作成するだけです。



(lambda (-plist) (apply f (cons x -plist)))


考え方は非常に単純です。ラムダ式を作成します。ラムダ式の本体では、パラメーターリストの元の関数を呼び出すだけで、ヘッドがxの値に置き換えられます。



つまり、関数の部分的な適用では、この関数に基づいてラムダ式を作成する必要があります。これにより、引数が1つ少なくなり、部分的な適用中に指定された引数は、ラムダ式の本体での関数の内部呼び出しで「考慮」されます。

これを実装する方法は?簡単... ​​HomeLispには、関数の定義式またはマクロへのアクセスを提供するgetd関数があります。



(defun g (x y z) (+ x (* x y) (* x y z)))

==> G
(getd 'g)

==> (EXPR (X Y Z) (+ X (* X Y) (* X Y Z)))


ご覧のとおり、getdは関数の定義式を返します。ラムダの「代わりに」、特別なEXPRアトムがあります。関数のパラメーターリスト(結果の2番目の要素)を取得してラムダ式を作成できます。ラムダ式のパラメーターは元のパラメーターリストの末尾になり、ラムダ式の本体で、パラメーターの完全なリストを使用して元の関数を呼び出します。



(defun part-apply (f a)
  (let* ((plist (cadr (getd f))) ;;   
         (rlist (cdr plist))     ;;     
         (clist (cons a rlist))) ;;      a
        `(lambda ,rlist (apply (quote ,f) (list ,@clist)))))


上記のコードから、plistが部分的に適用したい関数fの元のリストであることが簡単に理解できます。rlistは最初の要素のない元のリストであり、clistはパラメーターの完全なリストであり、最初の要素がxに置き換えられています。具体的には、上記の関数gの場合、plist =(xyz)、rlist =(yz)、およびclist =(ayz)です。次に、部分的なアプリケーションがどのように機能するかを確認しましょう。



(part-apply 'g 111)

==> (LAMBDA (Y Z) (APPLY (QUOTE G) (LIST 111 Y Z)))


これがまさに計画されたものであることがわかります。part-applyは、引数の数が1つ少ない新しい関数を返しました。この新しい関数がパラメーターyおよびzで呼び出された場合、結果は3つの引数で元の関数gを呼び出した場合とまったく同じです:111 yおよびz:



(g 111 1 2)

==> 444 ;;  

(funcall (part-apply 'g 111) 1 2) ;;  "--"

==> 444 

((part-apply 'g 111) 1 2) ;;  "-"

==> 444


以下では(簡潔にするために)funcallを指定しません。 HomeLispの次のコアでは、関数を計算するためのスキームが変更されます-「スキーム」構文が利用可能になります。



さらに進んでみましょう。再申請の結果を再申請したいという自然な欲求があります。これは非常に単純であることがわかります。結局のところ、部分的な適用の結果はラムダ式であり、getdによって返される結果とほぼ同じ構造になっています。ラムダ式のパラメータリストは、リストの2番目の項目です。これらすべての考慮事項により、問題の最終的な解決策を構築できます。



(defun part-apply (f a)
  (cond ((and (atom f) (member 'expr (proplist f))) ;; ***  ***
         (let* ((plist (cadr (getd f))) ;;   
                (rlist (cdr plist))          ;;     
                (clist (cons a rlist)))    ;;      a 
               `(lambda ,rlist (apply (quote ,f) (list ,@clist)))))
        ((eq 'lambda (car f))   ;; *** - ***
         (let* ((plist (cadr f))        ;;   
                (rlist (cdr plist))       ;;     
                (clist (cons x rlist))) ;;      x
               `(lambda ,rlist (apply (quote ,f) (list ,@clist)))))
        (t (raiseerror "part-apply:   "))))


ここで、最初のパラメーターの分析が追加されます。それがアトムの場合、(EXPRタイプの)関数を「表す」必要があります。最初のパラメーターが「有効な」アトムでもラムダ式でもない場合、エラー条件が発生します。もちろん、コードをさらに短くすることもできます。わかりやすくするために、ほぼ同じ2つのブランチを残しています。次に、この関数で何ができるかを見てみましょう。



(part-apply (part-apply 'g 1) 2) ;;      

==> (LAMBDA (Z) (APPLY (QUOTE (LAMBDA (Y Z) (APPLY (QUOTE G) (LIST 1 Y Z)))) (LIST 2 Z)))

((part-apply (part-apply 'g 1) 2) 3) ;;       

==> 9

(part-apply (part-apply (part-apply 'g 111) 1) 2) ;;     

==> (LAMBDA NIL (APPLY (QUOTE (LAMBDA (Z) (APPLY (QUOTE (LAMBDA (Y Z) (APPLY (QUOTE G) (LIST 111 Y Z)))) (LIST 1 Z)))) (LIST 2)))

((part-apply (part-apply (part-apply 'g 111) 1) 2)) ;;    ...

==> 444

(setq u (part-apply 'g 111)) ;;      

==> (LAMBDA (Y Z) (APPLY (QUOTE G) (LIST 111 Y Z)))
;;    U

(part-apply u 1) ;;    

==> (LAMBDA (Z) (APPLY (QUOTE (LAMBDA (Y Z) (APPLY (QUOTE G) (LIST 111 Y Z)))) (LIST 1 Z)))

((part-apply u 1) 2) ;;   

==> 444


それではカレーをしましょう。不十分な数の引数を受け入れるカレー関数は、残りを処理できる関数を返します。カレーの主な目的は、部分的なアプリケーションを提供することです。もう一方の端から行きました。次に、部分的なアプリケーションがある場合にカレーを実装する方法を見てみましょう。



いくつかの引数の関数gが与えられているとします。別の名前でカレーバージョンを作成します!G。構成の本質は次のとおりです。関数!Gは無数の引数を取ります。呼び出し後、渡された引数の数を確認する必要があります。この数が元の関数gの引数の数と等しい場合、fromを入力gに渡す必要があります。 gが受け入れることができるよりも多くの引数がある場合、エラー条件が発生するはずです。しかし、引数の数が関数gの数より少ない場合は、...はい、はい-順次部分的なアプリケーションを実行する必要があります。このアプリケーションの結果を返します。



この場合、マクロを使用すると便利です。コメント付きのコードは以下のとおりです。



(defmacro curry (f)
   (let* ((parmlist (cadr (getd f))) ;;   f
           (body      (cddr (getd f))) ;;   f
	   (lp          (length parmlist)) ;;    f
	   (cf          (implode (cons '! (explode f))))) ;;     
`(defun ,cf (&rest x)
   (let ((lx (length x))) ;;    
        (cond  ((= lx ,lp) ;;  
                    (let ((e `(lambda ,parmlist ,@body)))
                          (apply e x)))
                  ((> lx ,lp) ;;   
                          (raiseerror "curry:    "))
                  (t (let ((tmp nil)) ;;   
                (iter (for a in x) ;;    
	    (setq tmp (if (null tmp) (part-apply (quote ,f) a) (part-apply tmp a))))
                       tmp)))))))


ここで、新しい関数名の構成についてコメントする価値があります。これを行うには、HomeLisp explode関数を使用して、アトムから構成シンボルのリストを作成し、implodeを使用してリストシンボルを1つに圧縮し、新しいアトムを生成します。



それでは、実際のマクロを確認しましょう。



(curry g) ;;  g

==> !G ;;  

(!g 1 2 3) ;;   

==> 9 ;;    g

(!g 1 2) ;;    -     

==> (LAMBDA (Z) (APPLY (QUOTE (LAMBDA (Y Z) (APPLY (QUOTE G) (LIST 1 Y Z)))) (LIST 2 Z)))

;;     :

((!g 1 2) 3) ;;   

==> 9

((!g 1) 2 3) ;;   

==> 9

(!g 1 2 3 4) ;;    

curry:    
==> ERRSTATE


カレーは追加の制御なしで行われました。また、カレーラムダ式は実装していません。あなたが望むなら、あなたはこれをすべて行うことができます...



これはあなたが多くの努力なしでLispで部分的な機能アプリケーションとカレーを実装することができる方法です。Lispは素晴らしい言語です!



清聴ありがとうございました。



All Articles