Lispに関するPaul Grahamのポイントのいくつかを説明してください


146

Paul GrahamのWhat What Lispのポイントのいくつかを理解するのに助けが必要です。

  1. 変数の新しい概念。Lispでは、すべての変数は事実上ポインタです。値は、変数ではなく型を持つものであり、変数の割り当てまたはバインドは、ポインタが指すものではなく、ポインタのコピーを意味します。

  2. シンボルタイプ。シンボルは文字列とは異なり、ポインタを比較することで等価性をテストできます。

  3. シンボルのツリーを使用するコードの表記。

  4. 言語全体が常に利用可能です。読み取り時、コンパイル時、および実行時に実際の違いはありません。読み取り中にコードをコンパイルまたは実行し、コンパイル中にコードを読み取りまたは実行し、実行時にコードを読み取りまたはコンパイルできます。

これらのポイントはどういう意味ですか?CやJavaなどの言語ではどのように違うのですか?Lispファミリ言語以外の他の言語には、これらの構成要素のいずれかがありますか?


10
関数型プログラミングタグがここで保証されているかどうかはわかりません。関数型コードを書くのと同じように、多くのLispで命令型またはOOコードを書くことも同様に可能であり、実際には非機能型Lispがたくさんあります。コード周り。代わりにfpタグを削除してclojureを追加することをお勧めします-うまくいけば、これがJVMベースのLispersから興味深い入力をもたらすかもしれません。
のMichałMarczyk

58
ここにもpaul-grahamタグがありますか?!!! 素晴らしい...
missingfaktor

@missingfaktor多分それはburninate-request

回答:


98

Mattの説明は申し分なく、CやJavaとの比較で写真を撮っていますが、私はしません-何らかの理由で、このトピックについてたまに話し合うのがとても楽しいので、これが私のショットです答えで。

ポイント(3)および(4)について:

リストのポイント(3)と(4)は、今でも最も興味深く、関連性があるようです。

それらを理解するために、実行する途中で、Lispコードで何が起こるかを明確に把握することは有用です-プログラマーによって入力された文字のストリームの形で-。具体的な例を見てみましょう:

;; a library import for completeness,
;; we won't concern ourselves with it
(require '[clojure.contrib.string :as str])

;; this is the interesting bit:
(println (str/replace-re #"\d+" "FOO" "a123b4c56"))

このClojureコードのスニペットが出力されaFOObFOOcFOOます。読み取り時間はユーザーコードに対して実際には開かれていないため、Clojureはおそらくリストの4番目の点を完全に満たしていないことに注意してください。ただし、これが別の方法であるとはどういう意味かについて説明します。

したがって、このコードがどこかのファイルにあり、Clojureに実行を依頼するとします。また、(簡単にするために)ライブラリのインポートを通過したと仮定します。興味深いビットは、右端から(println始まり)、右端で終わります。これは期待どおりに字句解析/解析されますが、すでに重要なポイントが発生します。結果は、コンパイラ固有の特別なAST表現ではありませんこれは、通常のClojure / Lispデータ構造、つまり一連のシンボルを含むネストされたリストです。文字列と-この場合-に対応する単一のコンパイル済み正規表現パターンオブジェクト#"\d+"リテラル(これについては以下で詳しく説明します)。一部のLispはこのプロセスに独自の小さな工夫を加えていますが、Paul Grahamは主にCommon Lispについて言及していました。質問に関連する点で、ClojureはCLに似ています。

コンパイル時の言語全体:

この後、すべてのコンパイラーは(Lispインタープリターについても同様です。Clojureコードは常にコンパイルされます)、Lispプログラマーが操作に使用するLispデータ構造です。この時点で素晴らしい可能性が明らかになります:Lispプログラマーが、Lispプログラムを表すLispデータを操作し、オリジナルの代わりに使用するために変換されたプログラムを表す変換されたデータを出力するLisp関数を作成できるようにしないでください。言い換えれば、Lispプログラマーが、Lispではマクロと呼ばれる一種のコンパイラプラグインとして関数を登録できるようにしてはどうでしょうか。実際、まともなLispシステムにはこの能力があります。

したがって、マクロは、実際のオブジェクトコードが出力される最後のコンパイルフェーズの前に、コンパイル時にプログラムの表現で動作する通常のLisp関数です。マクロが実行できるコードの種類に制限がないため(特に、実行するコード自体は、多くの場合、マクロ機能を自由に使用して記述されています)、「言語全体がコンパイル時に利用可能です。 」

読み取り時の言語全体:

その#"\d+"正規表現リテラルに戻りましょう。上記のように、これは、コンパイル時に準備される新しいコードの最初の言及をコンパイラーが聞く前に、読み取り時に実際のコンパイル済みパターンオブジェクトに変換されます。これはどのように起こりますか?

まあ、Clojureの現在の実装方法は、ポールグラハムが考えていたものとは多少異なりますが、巧妙なハックで何でも可能です。Common Lispでは、話は概念的には少しすっきりしています。基本は似ていますが、Lispリーダーは状態マシンであり、状態遷移を実行し、最終的に「受け入れ状態」に達したかどうかを宣言するだけでなく、文字が表すLispデータ構造を吐き出します。したがって、文字123は数字123などになります。重要なポイントは次のとおりです。この状態マシンはユーザーコードによって変更できます。(前に述べたように、CLの場合はそれが完全に当てはまります。Clojureの場合、ハック(推奨されず、実際には使用されません)が必要です。しかし、余談ですが、これはPGの記事であり、詳しく説明することになっています。)

したがって、Common Lispプログラマーで、Clojureスタイルのベクトルリテラルのアイデアが気に入った場合は、リーダーに関数を接続するだけで、一部の文字シーケンスに適切に反応し[#[場合によっては)、次のように扱うことができます。一致するで終了するベクトルリテラルの開始]。このような関数はリーダーマクロと呼ばれ、通常のマクロと同じように、以前に登録されたリーダーマクロによって有効にされたファンキー表記で記述されたコードを含め、あらゆる種類のLispコードを実行できます。だから、あなたのために読む時間には言語全体があります。

まとめ:

実際、これまでに実証されたことは、通常のLisp関数を読み取り時またはコンパイル時に実行できることです。ここから、読み取り、コンパイル、または実行時に読み取りとコンパイル自体がどのように可能かを理解するために必要な1つのステップは、読み取りとコンパイル自体がLisp関数によって実行されることを認識することです。呼び出しreadたりeval、いつでも、文字ストリームからLispデータを読み込んだり、Lispコードをコンパイルして実行したりできます。それが常にそこにある言語全体です。

リストのポイント(3)をLispが満たすという事実が、ポイント(4)をどうにかして満たす方法に不可欠であることに注意してください-Lispによって提供されるマクロの特定のフレーバーは、通常のLispデータによって表されるコードに大きく依存します。これは(3)によって可能になるものです。ちなみに、ここではコードの「ツリーっぽい」側面だけが本当に重要です。おそらく、XMLを使用してLispを作成することができます。


4
注意:「通常の(コンパイラー)マクロ」と言うことで、Common Lisp(少なくとも)で「コンパイラーマクロ」は非常に具体的で異なるものである場合、コンパイラーマクロが「通常の」マクロであることを意味します:lispworks。 com / documentation / lw51 / CLHS / Body /…
Ken

ケン:いいですね、ありがとう!私はそれを「通常のマクロ」に変更します。これは、だれもつまずきそうにありません。
のMichałMarczyk

素晴らしい答え。質問を何時間かグーグル/熟考するよりも、5分で多くのことを学びました。ありがとう。
チャーリーフラワーズ

編集:ああ、追加の文章を誤解した。文法が修正されました(編集を受け入れるには「ピア」が必要です)。
Tatiana Racheva

S式とXMLは同じ構造を指示できますが、XMLははるかに冗長であるため、構文としては適していません。
Sylwester 2013

66

1)変数の新しい概念。Lispでは、すべての変数は事実上ポインタです。値は、変数ではなく型を持つものであり、変数の割り当てまたはバインドは、ポインタが指すものではなく、ポインタのコピーを意味します。

(defun print-twice (it)
  (print it)
  (print it))

「それ」は変数です。任意の値にバインドできます。変数に関連付けられている制限やタイプはありません。関数を呼び出す場合、引数をコピーする必要はありません。変数はポインターに似ています。変数にバインドされている値にアクセスする方法があります。メモリを予約する必要はありません。関数を呼び出すときに、任意のデータオブジェクト(任意のサイズと任意の型)を渡すことができます。

データオブジェクトには「タイプ」があり、すべてのデータオブジェクトはその「タイプ」を照会できます。

(type-of "abc")  -> STRING

2)シンボルタイプ。シンボルは文字列とは異なり、ポインタを比較することで等価性をテストできます。

シンボルは、名前を持つデータオブジェクトです。通常、名前はオブジェクトの検索に使用できます。

|This is a Symbol|
this-is-also-a-symbol

(find-symbol "SIN")   ->  SIN

シンボルは実際のデータオブジェクトなので、同じオブジェクトかどうかをテストできます。

(eq 'sin 'cos) -> NIL
(eq 'sin 'sin) -> T

これにより、たとえば、記号付きの文を書くことができます。

(defvar *sentence* '(mary called tom to tell him the price of the book))

これで、文中のTHEの数を数えることができます。

(count 'the *sentence*) ->  2

Common Lispでは、シンボルは名前を持つだけでなく、値、関数、プロパティリスト、パッケージを持つこともできます。したがって、シンボルを使用して変数または関数に名前を付けることができます。プロパティリストは通常​​、メタデータをシンボルに追加するために使用されます。

3)シンボルのツリーを使用するコードの表記。

Lispは、その基本的なデータ構造を使用してコードを表します。

リスト(* 3 2)は、データとコードの両方にすることができます。

(eval '(* 3 (+ 2 5))) -> 21

(length '(* 3 (+ 2 5))) -> 3

木:

CL-USER 8 > (sdraw '(* 3 (+ 2 5)))

[*|*]--->[*|*]--->[*|*]--->NIL
 |        |        |
 v        v        v
 *        3       [*|*]--->[*|*]--->[*|*]--->NIL
                   |        |        |
                   v        v        v
                   +        2        5

4)常に利用可能な言語全体。読み取り時、コンパイル時、および実行時に実際の違いはありません。読み取り中にコードをコンパイルまたは実行し、コンパイル中にコードを読み取りまたは実行し、実行時にコードを読み取りまたはコンパイルできます。

Lispは、テキストからデータとコードを読み取るREAD関数、コードをロードするLOAD関数、コードを評価するEVAL関数、コードをコンパイルするCOMPILE関数、およびデータとコードをテキストに書き込むPRINT関数を提供します。

これらの機能は常に利用可能です。彼らは消えません。それらは任意のプログラムの一部にすることができます。つまり、すべてのプログラムがコードを読み取り、ロード、評価、または印刷できます-常に。

CやJavaなどの言語ではどのように違うのですか?

これらの言語では、シンボル、データとしてのコード、またはデータとしてのランタイム評価をコードとして提供していません。Cのデータオブジェクトは通常、型指定されていません。

LISPファミリ言語以外の他の言語には、これらの構成要素が含まれていますか?

多くの言語には、これらの機能のいくつかがあります。

違い:

Lispでは、これらの機能は使いやすいように言語に組み込まれています。


33

(1)と(2)については、彼は歴史的に話している。Javaの変数はほとんど同じなので、値を比較するために.equals()を呼び出す必要があります。

(3)はS式について話している。Lispプログラムはこの構文で記述されています。これは、JavaやCなどのアドホック構文に比べて多くの利点を提供します。たとえば、CマクロやC ++テンプレートよりはるかにクリーンな方法でマクロ内の繰り返しパターンをキャプチャし、同じコアリストでコードを操作します。データに使用する操作。

(4)Cを例にとると、言語は実際には2つの異なるサブ言語です:if()やwhile()のようなものと、プリプロセッサー。プリプロセッサを使用して、常に自分自身を繰り返す必要をなくすか、#if /#ifdefを使用してコードをスキップします。しかし、両方の言語はまったく別のものであり、#ifのようにコンパイル時にwhile()を使用することはできません。

C ++では、テンプレートを使用するとこれがさらに悪化します。コンパイル時にコードを生成する方法を提供するテンプレートメタプログラミングに関するいくつかのリファレンスを確認してください。これは、専門家以外の人が頭を回るのが非常に困難です。さらに、これは実際には、コンパイラーがファーストクラスのサポートを提供できないテンプレートとマクロを使用したハックとトリックの束です-単純な構文エラーを作成した場合、コンパイラーは明確なエラーメッセージを提供できません。

まあ、Lispを使用すると、これらすべてを1つの言語で行うことができます。初日と同じように、実行時にコードを生成します。これはメタプログラミングが取るに足らないことを示唆するものではありませんが、ファーストクラス言語とコンパイラーのサポートがあれば、間違いなくもっと簡単です。


7
また、このパワー(およびシンプルさ)は50年以上の歴史があり、初心者プログラマーが最小限のガイダンスでそれを実行し、言語の基礎について学ぶことができるように実装するのに十分簡単です。Java、C、Python、Perl、Haskellなどが同様の優れた初心者プロジェクトであるという同様の主張は聞こえません。
マット・カーティス

9
Java変数はLispシンボルのようにはまったく思えません。Javaには記号の表記はありません。変数を使用してできることは、その値のセルを取得することだけです。それもなど、彼らは、引用して評価、渡すことができるかどうかについての話、しても意味がありませんので、文字列を抑留することができますが、それらは通常、名前じゃない
ケン・

2
40歳以上の方がより正確かもしれません:)、@ケン:彼は1)Javaの非プリミティブ変数は参照によるもので、lispに似ています、2)Javaのインターンされた文字列はlispのシンボルに似ています-もちろん、あなたが言ったように、Javaでインターンされた文字列/コードを引用したり評価したりすることはできないので、それらはまだかなり異なります。

3
@Dan -最初の実装をまとめましたが、初期わからないときマッカーシー紙記号計算上は1960年に出版された
Inaimathi

Javaは、Foo.class / foo.getClass()の形式で「シンボル」を部分的または不規則にサポートしています。つまり、タイプのClass <Foo>オブジェクトは、列挙値と同様に、少し類似しています。程度。しかし、Lispシンボルの非常に最小限の影。
BRPocock 2012

-3

ポイント(1)および(2)もPythonに適合します。単純な例 "a = str(82.4)"を例に取ると、インタープリターは最初に値82.4の浮動小数点オブジェクトを作成します。次に、文字列コンストラクターを呼び出し、値'82 .4 'の文字列を返します。左側の「a」は、その文字列オブジェクトのラベルにすぎません。元の浮動小数点オブジェクトへの参照がなくなったため、元の浮動小数点オブジェクトはガベージコレクションされました。

Schemeでは、すべてが同様の方法でオブジェクトとして扱われます。Common Lispについてはよくわかりません。C / C ++の概念について考えないようにします。Lispの美しいシンプルさに頭を悩ませようとしたときに、ヒープが遅くなりました。

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