LETとLET *(並列バインディングと順次バインディング)の違いを理解しており、理論的には完全に理にかなっています。しかし、実際にLETが必要になったことがありますか?最近見たすべてのLISPコードで、すべてのLETを変更せずにLET *に置き換えることができます。
編集:OK、わかりましたwhy誰かがマクロと思われるLET *を発明したのはいつ頃か。私の質問は、LET *が存在することを考えると、LETが留まる理由はありますか? LET *がプレーンLETと同様に機能しない実際のLISPコードを記述しましたか?
私は効率性の議論を買いません。まず、LET *をLETと同じくらい効率的なものにコンパイルできるケースを認識することは、それほど難しくないようです。第二に、CL仕様には、効率性を重視して設計されているようには思えないことがたくさんあります。 (型宣言のあるLOOPを最後に見たのはいつですか?それらを使用するのを見たことがありません。)1980年代後半のDick Gabrielのベンチマークの前は、CL wasまったく遅いです。
これは後方互換性の別のケースのように見えます:賢明なことに、誰もLETのような基本的なものを壊すことを恐れませんでした。これは私の予感でしたが、LETがLET *よりも途方もなく簡単なものをたくさん作ってくれた、私が見逃していた愚かで単純なケースを誰も持っていないということを聞いて安心します。
LET
自体は、Functional Programming Languageの実際のプリミティブではありません。これは、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
として宣言しています。
リチャードP.ガブリエルは1985年にLISPベンチマークに関する本を出版しましたが、当時これらのベンチマークは非CL Lispでも使用されていました。 Common LISP自体は1985年にまったく新しいものでした-言語を説明したCLtL1本は1984年に発行されたばかりです。その時。実装された最適化は、基本的に以前の実装(MacLispなど)と同じ(またはそれ以下)でした。
しかし、LET
対LET*
の主な違いは、LET
を使用するコードは、バインディング句が相互に独立しているため、人間にとって理解しやすいことです。 (副作用として変数を設定しない)。
あなたはneedしませんが、通常はwantします。
LETは、トリッキーなことは何もせずに標準の並列バインディングを実行していることを示唆しています。 LET *はコンパイラに制限を課し、ユーザーにシーケンシャルバインディングが必要な理由があることを提案します。 styleに関しては、LET *によって課せられる追加の制限を必要としない場合、LETの方が優れています。
LET *よりもLETを使用する方が効率的です(コンパイラ、オプティマイザなどによって異なります)。
(上記の箇条書きは、別のLISP方言であるSchemeに適用されます。clispは異なる場合があります。)
私は人為的な例に耐えています。この結果を比較します。
(print (let ((c 1))
(let ((c 2)
(a (+ c 1)))
a)))
これを実行した結果:
(print (let ((c 1))
(let* ((c 2)
(a (+ c 1)))
a)))
LISPでは、可能な限り弱い構成を使用したいという要望がしばしばあります。たとえば、比較する項目が数値であることがわかっている場合、一部のスタイルガイドでは、eql
ではなく=
を使用するように指示されます。多くの場合、コンピューターを効率的にプログラムするのではなく、意味を指定することが目的です。
ただし、実際の効率の改善は、あなたが意味することだけを言って、より強力な構造を使用しないことで実現できます。 LET
を使用した初期化がある場合は、それらを並行して実行できますが、LET*
初期化は順次実行する必要があります。実装が実際にそれを行うかどうかはわかりませんが、将来的にはうまくいくかもしれません。
LETとLET *の共通リストの主な違いは、LETのシンボルが並行してバインドされ、LET *のシンボルが順番にバインドされることです。 LETを使用すると、init-formsを並列に実行することも、init-formsの順序を変更することもできません。理由はCommon LISPでは、関数に副作用を持たせることができます。したがって、評価の順序は重要であり、フォーム内では常に左から右です。したがって、LETでは、最初にinit-formsが左から右に評価され、次にバインディングが作成され、 左から右へ 並行して。 LET *では、init-formが評価され、左から右に順番にシンボルにバインドされます。
私は最近、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*
を使用した場合、誤ってa
とb
を同じ値に設定してしまいます。
さらに一歩進んで bind を使用して、let
、let*
、multiple-value-bind
、destructuring-bind
など。さらに拡張可能です。
一般的に、私は「最も弱い構成」を使用するのが好きですが、コードにノイズを与えるだけなので、let
および友人とは使用しません(主観的な警告!私に反対を説得する必要はありません...)
(let ((list (cdr list))
(pivot (car list)))
;quicksort
)
もちろん、これはうまくいくでしょう:
(let* ((rest (cdr list))
(pivot (car list)))
;quicksort
)
この:
(let* ((pivot (car list))
(list (cdr list)))
;quicksort
)
しかし、それは重要な考えです。
おそらくlet
を使用することにより、コンパイラは、おそらくスペースまたは速度の改善のために、コードを並べ替える柔軟性が高くなります。
スタイル的に、並列バインディングを使用すると、バインディングがグループ化されるという意図が示されます。これは、動的バインディングの保持に使用されることがあります。
(let ((*PRINT-LEVEL* *PRINT-LEVEL*)
(*PRINT-LENGTH* *PRINT-LENGTH*))
(call-functions that muck with the above dynamic variables))
OPは「実際にLETを必要としたことがありますか」と尋ねますか?
Common LISPが作成されたとき、さまざまな方言で既存のLISPコードが大量にありました。 Common LISPを設計した人々が受け入れた概要は、共通の基盤を提供するLISPの方言を作成することでした。既存のコードをCommon LISPに簡単に移植できるようにするために「必要」です。 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 c b a)はうまくいかないからです。しかし、これらのマクロはさまざまな理由でかなりずさんです。そのため、「実際の必要性」の議論が損なわれます。
Rainer Joswig's answerに加えて、純粋主義的または理論的な観点から。 Let&Let *は2つのプログラミングパラダイムを表します。それぞれ機能的および順次的。
Letの代わりにLet *を使用し続ける理由については、私が一日中ほとんどの時間を仕事で費やしているシーケンシャル言語とは対照的に、あなたは家に戻って純粋な関数型言語で考えることから楽しみを奪っています:)
let
演算子は、指定するすべてのバインディングに単一の環境を導入します。 let*
、少なくとも概念的に(そして宣言をしばらく無視する場合)、複数の環境を導入します:
それは言うことです:
(let* (a b c) ...)
のようなものです:
(let (a) (let (b) (let (c) ...)))
ある意味で、let
はより原始的ですが、let*
はlet
- sのカスケードを記述するための構文糖衣です。
しかし、気にしないでください。 (そして、私たちが「気にしない」理由については、後で正当化する)。事実、2つの演算子があり、コードの「99%」では、どちらを使用してもかまいません。 let*
よりもlet
を優先する理由は、最後にぶら下がる*
がないためです。
2つの演算子があり、その名前に*
がぶら下がっているときはいつでも、*
なしの演算子を使用して、その状況で機能する場合、コードのlessさを抑えます。
これですべてです。
そうは言っても、let
とlet*
が意味を交換した場合、let*
を使用することは今よりもまれになると思われます。シリアルバインディングの動作は、それを必要としないほとんどのコードを煩わせません。ほとんどの場合、let*
を使用してパラレル動作を要求する必要はほとんどありません(このような状況は、シャドウイングを避けるために変数の名前を変更することで修正できます)。
さて、約束の議論のために。 let*
は概念的に複数の環境を導入しますが、let*
コンストラクトをコンパイルして単一の環境を生成するのは非常に簡単です。それにもかかわらず(少なくともANSI CL宣言を無視する場合)、単一のlet*
が複数のネストされたlet
- sに対応する代数的等価性があり、let
をより見やすくします。原始的、実際にlet*
をlet
に展開してコンパイルする理由はなく、悪い考えですらあります。
もう1つ:Common LISPのlambda
は実際にlet*
のようなセマンティクスを使用していることに注意してください!例:
(lambda (x &optional (y x) (z (+1 y)) ...)
ここでは、x
のy
init-formは以前のx
パラメーターにアクセスし、同様に(+1 y)
は以前のy
オプションを参照します。言語のこの領域では、バインディングのシーケンシャルな可視性に対する明確な好みが見えます。 lambda
内のフォームがパラメーターを認識できなかった場合、あまり役に立ちません。オプションのパラメーターは、以前のパラメーターの値に関して簡単にデフォルト設定することはできませんでした。
並列バインディングを使用してみましょう。
(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)
let
の下では、式を初期化する変数はすべて、まったく同じ字句環境、つまりlet
を囲む環境を参照します。これらの式がたまたま字句クロージャをキャプチャする場合、それらはすべて同じ環境オブジェクトを共有できます。
let*
では、すべての初期化式は異なる環境にあります。連続する式ごとに、環境を拡張して新しい式を作成する必要があります。少なくとも抽象的なセマンティクスでは、クロージャがキャプチャされる場合、それらは異なる環境オブジェクトを持ちます。
let*
は、let
の日常的な代替品として適切に使用するために、不要な環境拡張を折りたたむように十分に最適化する必要があります。どのフォームが何にアクセスし、独立したすべてのフォームをより大きな結合されたlet
に変換するコンパイラーが必要です。
(これは、let*
がカスケードlet
フォームを出力する単なるマクロ演算子である場合にも当てはまります。これらのカスケードlet
sで最適化が行われます)。
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
をシャドウしているため、nil
に2を追加します。このような変換canは、これらすべての変数のアルファ名を変更した場合に実行されます。その後、環境は適切に平坦化されます。
(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
そのためには、デバッグサポートを考慮する必要があります。プログラマーがデバッガーを使用してこのレキシカルスコープに入る場合、a
の代わりに#:g01
を処理する必要がありますか。
基本的に、let*
は複雑な構造であり、let
に縮小できる場合にlet
と同様に実行するために最適化する必要があります。
それだけでは、let*
よりもlet
を優先することにはなりません。優れたコンパイラーがあると仮定しましょう。常にlet*
を使用しないのはなぜですか?
一般的な原則として、エラーを起こしやすい低レベルの構造よりも生産性を高めてミスを減らす高レベルの構造を優先し、可能な限り高レベルの構造の適切な実装に依存して、ほとんど犠牲を払わないようにする必要がありますパフォーマンスのためのそれらの使用。それが、そもそもLISPのような言語で作業している理由です。
let*
はlet
に比べて明らかに高いレベルの抽象化ではないため、その推論はlet*
対let
にはうまく当てはまりません。それらは「等しいレベル」についてです。 let*
を使用すると、let
に切り替えるだけで解決するバグを導入できます。そしてその逆。 let*
は、let
ネストを視覚的に折りたたむための穏やかな構文糖であり、重要な新しい抽象化ではありません。