CommonLispでのLET対LET *


81

私はLETとLET *(並列結合と順次結合)の違いを理解しており、理論的な問題としては完全に理にかなっています。しかし、実際にLETが必要になったことがある場合はありますか?私が最近見たすべてのLispコードでは、すべてのLETを変更なしでLET *に置き換えることができます。

編集:わかりました、なぜ誰かがLET *を、おそらくマクロとして、昔に発明したのか理解しています。私の質問は、LET *が存在することを考えると、LETが留まる理由はありますか?LET *がプレーンなLETと同じように機能しない実際のLispコードを書いたことがありますか?

私は効率性の議論を買いません。まず、LET *をLETと同じくらい効率的なものにコンパイルできるケースを認識するのはそれほど難しいことではないようです。第二に、CL仕様には、効率を中心に設計されたようには見えないものがたくさんあります。(型宣言付きのLOOPを最後に見たのはいつですか?それらが使用されているのを見たことがないので、理解するのが非常に困難です。)1980年代後半のDick Gabrielのベンチマークの前、CLまったく遅かったです。

これは下位互換性の別のケースのようです。賢明なことに、LETほど基本的なものを壊すリスクを冒したいと思った人はいませんでした。それは私の予感でしたが、LETがLET *よりも途方もなく簡単にたくさんのことをしたという私が見逃した愚かな単純なケースを誰も持っていないことを聞いて安心しています。


並列は単語の選択としては不適切です。以前のバインディングのみが表示されます。並列バインディングは、Haskellの「... where ...」バインディングに似ています。
jrockway 2009

私は混乱させるつもりはありませんでした。それがスペックで使われている言葉だと思います。:-)
ケン

12
パラレルは正しいです。それは、バインディングが同時に生き返り、お互いを見たり、お互いに影を落としたりしないことを意味します。LETで定義された変数の一部を含み、他の変数を含まない、ユーザーに表示される環境は存在しません。
kaz 2012年

バインディングがletrecに似ているHaskells。彼らは同じスコープレベルですべてのバインディングを見ることができます。
Edgar Klerks 2014年

1
let必要な場合はありますか?」「複数の引数を持つ関数が必要な場合はありますか?」と尋ねるのと少し似ています。 letlet*プログラミング時に人間が他の人間に意図を伝えることができるため、効率性の概念が存在するため、存在しません。
tfb 2018

回答:


86

LETで置き換えることができるため、それ自体は関数型プログラミング言語の実際のプリミティブではありませんLAMBDA。このような:

(let ((a1 b1) (a2 b2) ... (an bn))
  (some-code a1 a2 ... an))

と類似しています

((lambda (a1 a2 ... an)
   (some-code a1 a2 ... an))
 b1 b2 ... bn)

だが

(let* ((a1 b1) (a2 b2) ... (an bn))
  (some-code a1 a2 ... an))

と類似しています

((lambda (a1)
    ((lambda (a2)
       ...
       ((lambda (an)
          (some-code a1 a2 ... an))
        bn))
      b2))
   b1)

どちらが簡単か想像できます。LETではありませんLET*

LETコードの理解が容易になります。多数のバインディングが表示され、「効果」(再バインディング)の上下/左右の流れを理解しなくても、各バインディングを個別に読み取ることができます。LET*バインディングが独立していないというプログラマー(コードを読み取るプログラマー)へのシグナルを使用しますが、ある種のトップダウンフローがあります-これは物事を複雑にします。

Common Lispには、のバインディングの値LETが左から右に計算されるという規則があります。関数呼び出しの値がどのように評価されるか(左から右)。したがって、LETは概念的に単純なステートメントであり、デフォルトで使用する必要があります。

のタイプLOOP?かなり頻繁に使用されます。覚えやすい型宣言のいくつかのプリミティブ形式があります。例:

(LOOP FOR i FIXNUM BELOW (TRUNCATE n 2) do (something i))

上記は、変数iがであると宣言していますfixnum

Richard P. Gabrielは、1985年にLispベンチマークに関する本を出版しました。当時、これらのベンチマークは非CLLispでも使用されていました。Common Lisp自体は1985年に真新しいものでした。言語を説明したCLtL1の本は、1984年に出版されたばかりです。当時、実装があまり最適化されていなかったのも不思議ではありません。実装された最適化は、基本的に以前の実装と同じ(またはそれ以下)でした(MacLispなど)。

しかし、LETLET*の主な違いはLET、バインディング句が互いに独立しているため、コードの使用が人間にとって理解しやすいことです-特に左から右への評価を利用するのは悪いスタイルであるため(変数をサイドとして設定しないため)効果)。


3
いやいや!ラムダは、LETで置き換えることができるため、実際のプリミティブではありません。また、引数値を取得するためのAPIを提供するだけの低レベルのラムダ: (low-level-lambda 2 (let ((x (car %args%)) (y (cadr args))) ...) :)
Kaz

36

LETは必要ありません、通常は必要です

LETは、トリッキーなことは何も行わずに、標準の並列バインディングを実行していることを示しています。LET *はコンパイラーに制限を課し、順次バインディングが必要な理由があることをユーザーに提案します。スタイルに関しては、LET *による追加の制限が必要ない場合はLETの方が適しています。

LET *よりもLETを使用する方が効率的です(コンパイラー、オプティマイザーなどによって異なります)。

  • 並列バインディングは並列で実行できます(ただし、LISPシステムが実際にこれを実行するかどうかはわかりません。また、initフォームは引き続き順次実行する必要があります)。
  • 並列バインディングは、すべてのバインディングに対して単一の新しい環境(スコープ)を作成します。シーケンシャルバインディングは、すべてのバインディングに対して新しいネストされた環境を作成します。並列バインディングは、使用するメモリ少なく変数の検索高速です

(上記の箇条書きはScheme、別のLISP方言に適用されます。clispは異なる場合があります。)


2
注:最初のプルレットポイントが誤解を招く理由についての説明については、この回答(および/またはhyperspecのリンクされたセクション)を参照してください。バインディングは、並行して起こるが、形態が順次実行される-スペックあたり。
lindes 2012

6
並列実行は、CommonLisp標準が処理するものではありません。より高速な変数ルックアップも神話です。
Rainer Joswig 2012

1
違いはコンパイラにとって重要なだけではありません。私はletとlet *を、何が起こっているのかを自分自身にヒントとして使用します。コードでletを見ると、バインディングが独立していることがわかります。let*を見ると、バインディングが相互に依存していることがわかります。しかし、私が知っているのは、letとlet *を一貫して使用するようにしているからです。
エレミヤ

28

私は不自然な例を持って来ます。この結果を比較してください:

(print (let ((c 1))
         (let ((c 2)
               (a (+ c 1)))
           a)))

これを実行した結果:

(print (let ((c 1))
         (let* ((c 2)
                (a (+ c 1)))
           a)))

3
なぜこれが当てはまるのかを開発することに注意してください。
John McAleely 2009年

4
@John:最初の例では、aのバインディングはの外部値を参照しcます。2番目の例ではlet*、バインディングが前のバインディングを参照できるようになっていますが、aのバインディングはの内部値を参照していますc。ローガンは、これが不自然な例であると嘘をついているわけではなく、役に立つふりさえしていません。また、インデントは非標準で誤解を招くものです。どちらの場合も、a'sと一致するように、'バインディングは1スペース上にある必要がcあり、内部の 'body'はそれ自体letから2スペースだけ離れている必要がありletます。
zem 2010年

3
この答えは重要な洞察を提供します。一つは、考え、具体的使用let1がしたいときに避ける(これで私は最初の1つだけではない意味)二次バインディングを有する結合最初に参照していますが、ないシャドウしたい以前の結合-のいずれかを初期化するためにそれの前の値を使用して二次バインディング。
lindes 2012

2
インデントがオフになっていますが(編集できません)、これがこれまでの最良の例です。私が見ると(...)、どのバインディングも互いに構築されておらず、別々に扱うことができることがわかります。私が見ているとき(let * ...)私は常に注意深くアプローチし、どのバインディングが再利用されているかを非常に注意深く見ています。この理由だけで、絶対にネストする必要がない限り、常に(let)を使用することは理にかなっています。
アンドリュー2012

1
(6年後...)間違っていたとして意図した設計により、インデント誤解を招く落とし穴?私はそれを修正するためにそれを編集する傾向があります...私はすべきではありませんか?
ネス

11

LISPでは、可能な限り弱い構造を使用したいという要望がよくあります。一部のスタイルガイドでは、たとえば、比較された項目が数値であることがわかっている場合=ではなく、使用するように指示されますeql。多くの場合、コンピュータを効率的にプログラムするのではなく、意味を指定するという考え方です。

ただし、より強力な構成を使用せずに、意味することだけを言うと、実際の効率が向上する可能性があります。を使用した初期化がある場合LET、それらは並行して実行できますが、LET*初期化は順次実行する必要があります。実際にそれを行う実装があるかどうかはわかりませんが、将来的にはうまくいく可能性があります。


2
いい視点ね。Lispは非常に高水準の言語であるため、「可能な限り最も弱い構成」がLispの土地でこれほど望ましいスタイルである理由を不思議に思います。Perlプログラマーが「まあ、ここで正規表現を使用する必要ありません...」と言っているのを見ることはありません:-)
Ken

1
わかりませんが、明確なスタイルの好みがあります。できるだけ同じ形式を使用したい人(私のような)は、多少反対しています(setfの代わりにsetqを書くことはほとんどありません)。それはあなたが何を意味するかを言うという考えと関係があるかもしれません。
David Thornley

=オペレータは、どちらも強くもより弱いですeql。に0等しいため、これは弱いテスト0.0です。ただし、数値以外の引数が拒否されるため、より強力です。
kaz 2012

2
あなたが言及している原則は、最も弱いものではなく、最も強い適用可能なプリミティブを使用することです。たとえば、比較対象がシンボルの場合は、を使用します。または、シンボリックな場所に割り当てていることがわかっている場合は、を使用します。しかし、この原則は、時期尚早の最適化なしに高水準言語を必要としている私の多くのLispプログラマーにも拒否されています。eqsetq
kaz 2012

1
実際、CLHSはバインディングは並列に実行されますと言いますが、「init-form-1init-form-2などの式は、[特定の左から右(または上)で[評価]されます] -ダウン)]注文」。したがって、値は順番に計算する必要があります(すべての値が計算された後にバインディングが確立されます)。RPLACDのような構造変異は言語の一部であり、真の並列処理では非決定論的になるため、これも理にかなっています。
ネスになります

9

私は最近、2つの引数の関数を書いていました。どちらの引数が大きいかがわかっている場合、アルゴリズムは最も明確に表現されます。

(defun foo (a b)
  (let ((a (max a b))
        (b (min a b)))
    ; here we know b is not larger
    ...)
  ; we can use the original identities of a and b here
  ; (perhaps to determine the order of the results)
  ...)

仮には、b我々が使用したい場合は、大きかったlet*、我々が誤って設定されているだろうab同じ値に設定します。


1
後でアウターの内側でxとyの値が必要にならない限りlet、これはより簡単に(そして明確に)行うことができます。-(rotatef x y)悪い考えではありませんが、それでもストレッチのように見えます。
ケン

それは本当だ。xとyが特別な変数である場合は、より便利な場合があります。
サミュエルエドウィンワード

9

LETとLET *の共通リストの主な違いは、LETのシンボルは並列にバインドされ、LET *のシンボルは順番にバインドされることです。LETを使用すると、init-formを並行して実行したり、init-formの順序を変更したりすることはできません。その理由は、CommonLispは関数に副作用を持たせることができるからです。したがって、評価の順序は重要であり、フォーム内では常に左から右になります。したがって、LETでは、init-formが最初に左から右に評価され、次にバインディングが左から右に並行て作成されます。LET *では、init-formが評価され、左から右に順番にシンボルにバインドされます。

CLHS:特別オペレーターLET、LET *


2
この答えは、おそらくこの答えへの応答であることからいくらかのエネルギーを引き出しているように思われますか?また、リンクされた仕様によれば、init-formsが直列に実行されると言うのは正しいですが、バインディングは並列に実行されると言われLETています。それが既存の実装に実際的な違いがあるかどうかはわかりません。
lindes 2012

8

私は一つの更なるステップや使用行くバインドをを統一することをletlet*multiple-value-binddestructuring-bindなど、及びそれも拡張可能です。

一般的に私は「最も弱い構成」を使用するのが好きですletが、コードにノイズを与えるだけなので、友達とは好きではありません(主観的な警告!反対のことを私に納得させようとする必要はありません...)


ああ、きちんと。今からBINDで遊びに行きます。リンクをありがとう!
ケン

4

おそらくletコンパイラを使用することで、おそらくスペースや速度を改善するために、コードを並べ替える柔軟性が高まります。

様式的には、並列バインディングを使用すると、バインディングがグループ化されるという意図が示されます。これは、動的バインディングを保持するために使用されることがあります。

(let ((*PRINT-LEVEL* *PRINT-LEVEL*)
      (*PRINT-LENGTH* *PRINT-LENGTH*))
  (call-functions that muck with the above dynamic variables)) 

コードの90%では、を使用するか、を使用するLETかに違いはありませんLET*。したがって、を使用*すると、不要なグリフが追加されます。場合はLET*、並列バインダーだったとLETシリアルバインダーた、プログラマはまだ使用することになりLET、そして唯一の引き抜きLET*結合並列したい時。これはおそらくLET*まれになります。
kaz

実際、CLHSはlet'sinit -formの評価の順序指定します
ネスになります

4
(let ((list (cdr list))
      (pivot (car list)))
  ;quicksort
 )

もちろん、これはうまくいくでしょう:

(let* ((rest (cdr list))
       (pivot (car list)))
  ;quicksort
 )

この:

(let* ((pivot (car list))
       (list (cdr list)))
  ;quicksort
 )

しかし、重要なのはその考えです。


2

OPは「実際に必要なLET」を尋ねますか?

Common Lispが作成されたとき、さまざまな方言で既存のLispコードが大量にありました。Common Lispを設計した人々が受け入れた簡単な説明は、共通の基盤を提供するLispの方言を作成することでした。彼らは、既存のコードをCommonLispに簡単かつ魅力的に移植できるようにするために「必要」でした。LETまたはLET *を言語から除外することは、他のいくつかの美徳に役立つ可能性がありますが、その重要な目標を無視していたでしょう。

LET *よりもLETを使用します。これは、データフローがどのように展開されているかを読者に伝えるためです。私のコードでは、少なくとも、LET *が表示された場合、早期にバインドされた値が後のバインドで使用されることがわかります。いいえ、そうする必要がありますか。しかし、私はそれが役立つと思います。とはいえ、デフォルトでLET *に設定されているコードと、作成者が本当に望んでいたことを示すLETの外観を読んだことはめったにありません。つまり、たとえば2つの変数の意味を交換します。

(let ((good bad)
     (bad good)
...)

「実際の必要性」に近づく議論の余地のあるシナリオがあります。これはマクロで発生します。このマクロ:

(defmacro M1 (a b c)
 `(let ((a ,a)
        (b ,b)
        (c ,c))
    (f a b c)))

よりもうまく機能します

(defmacro M2 (a b c)
  `(let* ((a ,a)
          (b ,b)
          (c ,c))
    (f a b c)))

(M2 cba)はうまくいかないので。しかし、これらのマクロはさまざまな理由でかなりずさんです。そのため、「実際の必要性」の議論が損なわれます。


1

Rainer Joswigの答えに加えて、純粋主義者または理論的観点から。Let&Let *は、2つのプログラミングパラダイムを表します。それぞれ機能的および順次。

Letの代わりにLet *を使い続ける必要がある理由については、私が1日のほとんどを使って作業するシーケンシャル言語とは対照的に、家に帰って純粋な関数型言語で考えることを楽しんでいます:)


1

並列バインディングを使用できるように、

(setq my-pi 3.1415)

(let ((my-pi 3) (old-pi my-pi))
     (list my-pi old-pi))
=> (3 3.1415)

そして、Let *シリアルバインディングを使用すると、

(setq my-pi 3.1415)

(let* ((my-pi 3) (old-pi my-pi))
     (list my-pi old-pi))
=> (3 3)

はい、それがそれらの定義方法です。しかし、いつ前者が必要になりますか?あなたは実際には、piの値を特定の順序で変更する必要のあるプログラムを書いているのではないと思います。:-)
ケン

1

letオペレータは、それが指定するすべてのバインディングのための単一の環境を紹介します。let*、少なくとも概念的に(そして宣言をしばらく無視すると)、複数の環境が導入されます。

つまり、次のようになります。

(let* (a b c) ...)

のようなものです:

(let (a) (let (b) (let (c) ...)))

したがって、ある意味でletは、より原始的let*ですが、let-sのカスケードを記述するための構文糖衣です。

しかし、それを気にしないでください。(そして、なぜ私たちが「気にしない」べきなのかについては、後で正当化するつもりです)。事実、2つの演算子があり、コードの「99%」では、どちらを使用してもかまいません。好む理由letオーバーはlet*、それが持っていないということだけである*最後のダングリングを。

2つの演算子があり、1つが*名前にぶら下がっている場合は常に*、その状況で機能する場合は、コードを醜くしないように、演算子なしで使用してください。

それがすべてです。

そうは言っても、意味letlet*交換すれば、let*今よりも使うのは珍しいのではないかと思います。シリアルバインディングの動作は、それを必要としないほとんどのコードに影響let*を与えません。パラレル動作を要求するために使用する必要があることはめったにありません(このような状況は、シャドウイングを回避するために変数の名前を変更することで修正することもできます)。

今、その約束された議論のために。が、let*概念的には、複数の環境を紹介し、それがコンパイルするのは非常に簡単だlet*単一の環境が生成されるように構造を。したがって、(少なくともANSI CL宣言を無視した場合)、単一let*が複数のネストされたlet-sに対応する代数的等式があり、letより原始的に見えますが、実際に拡張let*letてコンパイルする理由はありません。悪いアイデア。

もう1つ:CommonLispはlambda実際には-のlet*ようなセマンティクスを使用していることに注意してください!例:

(lambda (x &optional (y x) (z (+1 y)) ...)

ここで、のxinit-formyは以前のxパラメーターにアクセスし、同様(+1 y)に以前のyオプションを参照します。言語のこの領域では、バインディングのシーケンシャルな可視性に対する明確な好みが透けて見えます。のフォームがlambdaパラメータを認識できない場合は、あまり役に立ちません。オプションのパラメーターは、前のパラメーターの値に関して簡潔にデフォルト設定できませんでした。


0

の下ではlet、すべての変数初期化式は、まったく同じ字句環境、つまりを囲む環境を参照しletます。これらの式がたまたま字句クロージャをキャプチャする場合、それらはすべて同じ環境オブジェクトを共有できます。

の下ではlet*、すべての初期化式は異なる環境にあります。連続する式ごとに、環境を拡張して新しい式を作成する必要があります。少なくとも抽象的なセマンティクスでは、クロージャがキャプチャされた場合、それらは異なる環境オブジェクトを持ちます。

let*日常的な代替として適しているためには、不要な環境拡張を折りたたむように十分に最適化する必要がありますlet。どのフォームが何にアクセスしているかを動作し、独立したフォームをすべてより大きな結合されたものに変換するコンパイラが必要letです。

(これはlet*、カスケードlet形式を発行する単なるマクロ演算子であっても当てはまります。最適化はそれらのカスケード形式で行われletます)。

適切なスコープの欠如が明らかになるため、初期化を行うための隠れた変数割り当てを使用しlet*て、単一のナイーブとして実装することはできませんlet

(let* ((a (+ 2 b))  ;; b is visible in surrounding env
       (b (+ 3 a)))
  forms)

これがになったら

(let (a b)
  (setf a (+ 2 b)
        b (+ 3 a))
  forms)

この場合は機能しません。内側bが外側をシャドウしているbので、2を追加することになりますnilます。この種の変換、これらすべての変数の名前をアルファ名変更すると実行できます。その後、環境は適切に平坦化されます。

(let (#:g01 #:g02)
  (setf #:g01 (+ 2 b) ;; outer b, no problem
        #:g02 (+ 3 #:g01))
  alpha-renamed-forms) ;; a and b replaced by #:g01 and #:g02

そのためには、デバッグサポートを検討する必要があります。プログラマーがデバッガーを使用してこの字句スコープにステップインする場合、の#:g01代わりに処理する必要がありaますか。

だから基本的に、 let*は、を実行するために、またletそれがに減少する可能性がある場合に、適切に最適化する必要がある複雑な構造letです。

それだけでは支持することを正当化しないでしょう letするlet*。優れたコンパイラがあるとしましょう。let*いつも使ってみませんか?

一般原則として、エラーが発生しやすい低レベルの構成よりも、生産性を高めてミスを減らす高レベルの構成を優先し、高レベルの構成の適切な実装に可能な限り依存して、犠牲にする必要がほとんどないようにする必要があります。パフォーマンスのためのそれらの使用。そもそも私たちがLispのような言語で働いているのはそのためです。

let比べて明らかに高レベルの抽象化ではないlet*ため、この推論は対にうまく適用されません。それらは「等しいレベル」についてです。を使用すると、に切り替えるだけで解決されるバグを導入できます。そして、その逆。実際には、ネストを視覚的に折りたたむための穏やかな構文糖衣であり、重要な新しい抽象化ではありません。let*letlet*letlet*let


-1

特にLET *が必要な場合を除いて、私は主にLETを使用しますが、通常はさまざまな(通常は複雑な)デフォルト設定を行うときに、明示的LETを必要とするコードを作成することがあります。残念ながら、便利なコード例は手元にありません。


-1

letfとletf *をもう一度書き直したいと思う人はいますか?アンワインドプロテクトコールの数?

シーケンシャルバインディングを最適化するのが簡単です。

多分それは環境に影響を与えますか?

動的な範囲で継続を許可しますか?

時々(let(xyz)(setq z 0 y 1 x(+(setq x 1)(prog1(+ xy)(setq x(1- x)))))(values()))

【うまくいくと思います】ポイントは、シンプルな方が読みやすいこともあります。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.