4

私は Lisp を学んでいて、Lisp の関数から変更された入力引数を返さなければなりません。

次の簡単な例を考えてみましょう:

(defun swap (l1 l2)
  (let ((temp))
    (setf temp l1)
    (setf l1 l2)
    (setf l2 temp)))

(setf a (list 1 2 3))
(setf b (list 7 8 9))
(swap a b)
(print a)
(print b)

変数への参照を関数に渡す方法がわからないため、機能しません。それはLispでも可能ですか?この関数はどのように解決できますか?


アップデート

;;; doesn't change original
(defun foo1 (x)
  (setf x (list 0 0 0)))

;;; but this does
(defun foo4 (x)
  (setf (car x) 0)
  (setf (cdr x) (list 0 0)))

変数を変更できるように参照渡ししたかった理由は、3 つの入力引数を持つ関数があり、その関数がそれらすべてを変更する必要がある場合、参照によって変更する方がエレガントだと思うからです。 3 つの変数のリストを返し、それらで元の変数を上書きします。

;;; more elegant function
(defun foo (x y z)
  ;;... some work ...

  ;; Lets PRETEND this does work
  (setf x new-x)
  (setf y new-y)
  (setf z new-z))

; after this, a,b,c will have new values
(foo a b c)

;;; less elegant function
(defun foo (x y z)
  ;; ... some work ...
  (list new-x new-y new-z))

; after this, I still will have to manually set a,b,c
(setf temp (foo a b c))
(setf a (nth 0 tmp))
(setf b (nth 1 tmp))
(setf c (nth 2 tmp))

私がこれを達成しようとしている理由を説明すると、ハノイの塔の宿題があるからです。「ディスク」を挿入および削除するために、3つのリストをstacks使用し、それらに対してpopand関数を使用することを考えていました。push関数を定義(move n source target temp)しましたが、変更を加えて再帰的に呼び出していn-1ます。問題は、再帰関数で Ipopまたはpushスタックを実行すると、外部のスタックに影響を与えないことです。move関数が移動後にスタックを返すようにしたい場合n、新しいスタックのリストを実際に返す必要があります (そのエレガントでない関数) 参照によってそれらを編集するのではなく (そのよりエレガントな関数)

関数型言語の適切な方法は何ですか?

4

5 に答える 5

9

まず第一に、Common Lisp だけでなく、関数型プログラミングまたは Lisp 全般を学んでいる場合は、それを行わないでください。状態を変更する関数を記述しようとしないでください。これは、関数型プログラミングが機能する方法ではありません。2 つの値を交換する関数が必要な場合は、それらを逆順に返す関数を記述します。

2 つの値を交換することにまだ興味がある場合は、いくつかの非常に良い提案について、この同様の質問を参照してください。最も重要なのは、マクロとマニュアル参照 (実際の値のラッパー) です。

ただし、これらの回答には、Common Lisp でのみ使用でき、他のほとんどの Lisp 方言では使用できない重要な概念が 1 つ含まれていませ。しかし、最初に、変数を関数に渡す 2 つの方法を思い出してみましょう。C++ で次の例を検討してください。

void f(int x) {
    ...
}
int a = 5;
f(a);

これは「値渡し」戦略として知られています。 の値がaパラメータにコピーxれます。また、xは単なるコピーであるため、 内で変更してもf()、元の変数では何も起こりませんa

ただし、C++ では次のこともできます。

void f(int& x) {    
    ...
}
int a = 5; 
f(a);

この戦略は「参照渡し」と呼ばれます。ここでは、存在するメモリ内の場所へのポインターを渡しaます。したがってx、 とaはメモリの同じ部分を指し、 を変更するxとも変更されaます。

Common Lisp を含む関数型言語では、参照によって関数に変数を渡すことはできません。では、どのようにsetf機能しますか?CLには、メモリ内の場所を定義する場所の概念「場所」とも呼ばれる)があることがわかりました。(特別な形式setfに展開されるマクロ) は、値ではなく場所で直接機能します。set

要約する:

  1. Common Lisp は、ほとんどの Lisp と同様に、関数に変数を値のみで渡すことのみを許可します。
  2. Lisp には、場所(メモリ内の場所) の概念があります。
  3. setf場所で直接動作し、変数を変更するために使用できます。機能の制限を克服するためにマクロを使用できます。

CL の一部の組み込み関数は、 、などの場所と、すべてのオブジェクト アクセサーを返すことができることに注意してくださいいくつかの例については、このページを参照してください。carcdraref

アップデート

あなたの新しい質問は、値を変更する場所です-参照による関数内または参照なしの外部。ただし、関数型プログラミングではこれらのどれも正しくありません。ここでの正解は、何も変更しないでください。FPでは通常、いくつかの状態変数がありますが、元の変数が変更されないように、その場で変更する代わりに、変更されたコピーを作成してさらに渡します。階乗を計算するための再帰関数の例を考えてみましょう:

(defun factorial-ex (x accum)
   (if (<= x 1) 
      accum
      (factorial-ex (- x 1) (* x accum))))

(defun factorial (x)
   (factorial-ex x 1))

factorial-exもう 1 つのパラメーター (計算の現在の状態を保持するためのアキュムレータ) を取る補助関数です。再帰呼び出しごとに、1 を減らしての現在の値をx掛けます。ただし、andの値は変更しません。新しい値を関数の再帰呼び出しに渡します。物理的には、関数呼び出しごとに 1 つずつ、多くのandのコピーがあり、どれも変更されることはありません。accumxxaccumxaccum

(特定のオプションを持つ一部の CL 実装は、上記のメモリ内の異なる場所に関するステートメントを壊す、いわゆる末尾呼び出しの最適化を使用する可能性があることに注意してください。ただし、現時点では心配する必要はありません。)

あなたのタスクでは、同じことができます。関数内または関数外の 3 つの変数を変更する代わりに、変更されたコピーを作成し、再帰呼び出しに渡します。命令型プログラミングでは変数とループを使用し、関数型プログラミングでは不変値と再帰を優先する必要があります。

于 2013-03-05T22:49:20.570 に答える
5

組み込みマクロrotatefは、次の機能を実行します。

(setf x 1)
(setf y 3)
;x = 1, y = 3
(rotatef x y)
;x = 3, y = 1

これを行う独自の関数を作成するには、マクロを作成することをお勧めします。

(defmacro my-swap (a b)
     `(let ((temp ,a))
          (setf ,a ,b)
          (setf ,b temp)))

ただし、Clayton が指摘したように、このマクロは「temp」という名前の変数に適用すると失敗します。したがって、 を使用gensymして新しい変数名を作成し (使用されていないことが保証されています)、それを実際に値を切り替えるセカンダリ マクロに渡すことができます。

(defmacro my-swap-impl (a b sym) ;;implementation of my-swap
          `(let ((,sym ,b)) ;evaluate the symbol and use it as a variable name
             (setf ,b ,a)
             (setf ,a ,sym)))

これは、一時変数名として機能する 3 番目の引数を受け入れる以前の swap マクロのバージョンです。これは単純なマクロから呼び出されます。

(defmacro my-swap (a b) ;;simply passes a variable name for use in my-swap-impl
          `(my-swap-impl ,a ,b ,(gensym)))

この設定は、変数のキャプチャから安全であることを除いて、前の設定とまったく同じように使用できます。

于 2013-03-05T20:50:26.357 に答える
3

まず第一に、自分の仕事を正しく理解していることを確認する必要があります。変更された入力を返すことは、入力を変更することと同じではありません。

変更された入力を返すことは簡単です。次の簡単な例を考えてみましょう。

(defun foo (bar)
  (1+ bar))

この関数は、bar1 を追加して変更された入力を返します。入力と変更ルーチンを取り、それを入力 (または入力) に適用する、より一般的な関数を考えることができます。そのような関数は次のように呼ばれapplyます:

CL-USER> (apply '1+ '(1))
2

ここで、関数に渡された変数の値を変更したい場合、Lisp は関数の適用に参照渡しや名前渡しではなく値渡しを使用するため、単純に行うことは実際には不可能です。そのため、このようなタスクは通常、setf名前による呼び出しを使用するような特殊または汎用の変更マクロで実行されます。

ただし、いくつかの限られたケースで役立つ可能性がある別の回避策があります。変数の値を変更することはできませんが、データ構造に格納されている値を変更することはできます (データ構造は値であり、コピー経由ではありません)。したがって、データ構造を関数に渡すと、その内部の値を変更できます。例えば、

(defun swap (v1 v2)
  (psetf (elt v1 0) (elt v2 0)
         (elt v2 0) (elt v1 0)))
CL-USER> (defvar *v1* #(0))
CL-USER> (defvar *v2* #(1))
CL-USER> (swap *v1* *v2*)
CL-USER> (format t "~A ~A" *v1* *v2*)
#(1) #(0)

ただし、繰り返しますが、このアプローチは、それが必要なものであることが本当にわかっている限られた数のシナリオでのみ適用できる場合があります。

于 2013-03-06T08:28:06.610 に答える
0

これは単なるコメントであり、回答ではありません。

「a、b、c を手動で設定」の部分は、destructuring-bind で少しは改善されるかもしれません。

ハノイの塔を「状態の変更」の方法で行うには、 のような移動関数を呼び出します。これにより(move n stacks 0 2)、n 個のディスクがスタック(elt stacks 0)から他のスタック(elt stacks 2)に移動され、「参照」の問題が回避されます。

マクロとして記述せずに like を呼び出したい場合(move n source target)、ソースとターゲットは、Lisp リストから実装するある種のカプセル化されたスタックのようなオブジェクトである必要があります。おそらく、それらにはデータ用のスロットと独自のプッシュ/ポップ メソッドがあり、それらのスロットは新しいメモリ位置を指しますが、スタック オブジェクト自体のメモリ位置は変更しません。C の null で終わる文字列からカプセル化された String クラスを実装するのと同じように、String クラスのユーザーは、二重ポインター (C の場合) や二重参照のような「二重参照関係」トリックに頼る必要がありません。関係: 「stacks という名前はリストを参照し、リストにはリストを参照するスロットがあります..」( (move n stacks 0 2))。

スタックを実装する 1 つの方法 (Emacs Lisp でのみテスト済み):

(defun make-hanoi-stack (&rest items)
  (cons items "unused slot"))
(defun hanoi-stack-push (item hanoi-stack)
  (push item (car hanoi-stack)))
(defun hanoi-stack-pop (hanoi-stack)
  (pop (car hanoi-stack)))
(defun hanoi-stack-contents (hanoi-stack)
  (car hanoi-stack))

スタックの使用:

(defun move-one-item (from-hanoi-stack to-hanoi-stack)
  (hanoi-stack-push (hanoi-stack-pop from-hanoi-stack)
                    to-hanoi-stack))

(let ((stack1 (make-hanoi-stack 1 2 3))
      (stack2 (make-hanoi-stack 4)))
  (move-one-item stack1 stack2)
  (print (hanoi-stack-contents stack1))
  (print (hanoi-stack-contents stack2)))
于 2013-06-20T12:42:18.210 に答える