Paul Grahamの What is LISP Different からのポイントのいくつかを理解する助けが必要です。
変数の新しい概念。 LISPでは、すべての変数は事実上ポインターです。値は変数ではなくタイプを持つものであり、変数の割り当てまたはバインドは、ポインターが指すものではなく、ポインターをコピーすることを意味します。
シンボルタイプ。シンボルを文字列と異なる点は、ポインターを比較することで同等性をテストできることです。
シンボルの木を使用したコードの表記。
言語全体が常に利用可能です。読み取り時、コンパイル時、および実行時の間に実際の区別はありません。読み取り中にコードをコンパイルまたは実行し、コンパイル中にコードを読み取りまたは実行し、実行時にコードを読み取りまたはコンパイルできます。
これらのポイントはどういう意味ですか? CやJavaのような言語ではどう違いますか?現在、LISPファミリー言語以外の他の言語には、これらの構成要素がありますか?
Mattの説明はまったく問題ありません。CとJavaとの比較でショットを撮りますが、私はそうはしませんが、何らかの理由でこのトピックについてたまに議論するのが好きなので、ここに私のショットがあります。答えで。
リストのポイント(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の現在の実装方法は、Paul Grahamが念頭に置いたものとは多少異なりますが、 巧妙なハック で何でも可能です。 Common LISPの場合、ストーリーは概念的に若干簡潔になります。ただし、基本は似ています。LISPReaderは状態マシンであり、状態遷移を実行し、最終的に「受け入れ状態」に到達したかどうかを宣言することに加えて、文字が表すLISPデータ構造を吐き出します。したがって、文字123
は数字123
などになります。重要な点は次のとおりです。このステートマシンはユーザーコードによって変更できます。 (前述のように、それはCLの場合に完全に当てはまります。Clojureの場合、ハック(実際には使用されていない)が必要です。しかし、私は脱線し、PGの記事を詳しく説明します。)
したがって、Common LISPプログラマであり、Clojureスタイルのベクトルリテラルのアイデアがたまたまある場合は、何らかの文字シーケンス([
または#[
おそらく-そして、一致する]
で終わるベクトルリテラルの開始として扱います。このような関数はreader macroと呼ばれ、通常のマクロと同様に、事前に登録されたファンキー表記で有効になったコードを含む、あらゆる種類のLISPコードを実行できますリーダーマクロ。したがって、読み取り時にすべての言語があります。
実際、これまでに実証されたのは、読み取り時またはコンパイル時に通常のLISP関数を実行できることです。読み取り、コンパイル、または実行時に読み取りとコンパイル自体がどのように可能であるかを理解するためにここからとる必要がある1つのステップは、読み取りとコンパイル自体がLISP関数によって実行されることを認識することです。いつでもread
またはeval
を呼び出すだけで、それぞれ文字ストリームからLISPデータを読み込んだり、LISPコードをコンパイルおよび実行したりできます。それは常に言語全体です。
リストからLISPがポイント(3)を満たすという事実は、ポイント(4)を満たすために管理する方法に不可欠であることに注意してください。これは(3)によって可能になります。ちなみに、ここではコードの「ツリーっぽい」側面のみが非常に重要です。XMLを使用してLISPを作成することも考えられます。
1)変数の新しい概念。LISPでは、すべての変数は実質的にポインターです。値は変数ではなく型を持つものであり、変数の割り当てまたはバインドはポインターをコピーすることであり、彼らが指す。
(defun print-twice (it)
(print it)
(print it))
「it」は変数です。任意の値にバインドできます。変数に関連する制限や型はありません。関数を呼び出す場合、引数をコピーする必要はありません。変数はポインターに似ています。変数にバインドされている値にアクセスする方法があります。メモリをreserveする必要はありません。関数を呼び出すときに、任意のデータオブジェクト(任意のサイズと任意の型)を渡すことができます。
データオブジェクトには「タイプ」があり、その「タイプ」についてすべてのデータオブジェクトを照会できます。
(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
一般的な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では、これらの機能は使いやすいように言語に組み込まれています。
ポイント(1)と(2)については、彼は歴史的に話しています。 Javaの変数はほとんど同じであるため、値を比較するには.equals()を呼び出す必要があります。
(3)はS式について話しています。 LISPプログラムはこの構文で記述されており、CマクロやC++テンプレートよりもはるかにきれいな方法でマクロの繰り返しパターンをキャプチャするなど、JavaおよびCなどのアドホック構文よりも多くの利点を提供します、データに使用するのと同じコアリスト操作でコードを操作します。
(4)Cを例にとると:言語は実際には2つの異なるサブ言語です:if()やwhile()のようなものとプリプロセッサ。プリプロセッサを使用して、常に自分自身を繰り返す必要を省くか、#if /#ifdefを使用してコードをスキップします。しかし、両方の言語はまったく別のものであり、#ifのようにコンパイル時にwhile()を使用することはできません。
C++はテンプレートでこれをさらに悪化させます。コンパイル時にコードを生成する方法を提供するテンプレートメタプログラミングに関するいくつかのリファレンスを確認してください。また、専門家でない人が頭を抱えるのは非常に困難です。さらに、実際にはテンプレートとマクロを使用したハックとトリックの集まりであり、コンパイラはファーストクラスのサポートを提供できません-単純な構文エラーを作成すると、コンパイラは明確なエラーメッセージを表示できません。
LISPを使用すると、これらすべてを1つの言語で使用できます。初日に学んだのと同じものを使用して、実行時にコードを生成します。これは、メタプログラミングが簡単なことを示唆するものではありませんが、ファーストクラス言語とコンパイラーのサポートにより確かに簡単です。