LISPがあれば、マクロシステムの実装を容易にしますか?


21

私はSICPからSchemeを学んでいますが、Schemeを作るものの大きな部分、さらにLISPが特別なのはマクロシステムであるという印象を受けています。しかし、マクロはコンパイル時に拡張されるので、C / Python / Java / whateverに同等のマクロシステムを作成しないのはなぜですか?たとえば、pythonコマンドをexpand-macros | python何にでもバインドできます。このコードは、マクロシステムを使用しないユーザーにも移植可能です。コードを公開する前にマクロを拡張するだけです。しかし、私が収集したC ++ / Haskellのテンプレートを除いて、私はそのようなものを知りません。実際には同じではありません。LISPがあれば、マクロシステムの実装を容易にしますか?


3
「コードはマクロシステムを使用していない人にも移植可能です。コードを公開する前にマクロを拡張するだけです。」-警告するだけで、これはうまく機能しない傾向があります。それらの他の人々はコードを実行できますが、実際にはマクロ拡張コードは理解するのが難しく、通常は変更するのが困難です。実際には、著者が人間の目に合わせて拡張コードを調整しておらず、実際のソースを調整しているという意味で、「不適切に作成」されています。Javaプログラマーに、Cプリプロセッサーを介してJavaコードを実行し、どの色に変わるかを見てみてください;
スティーブジェソップ

1
ただし、マクロは実行する必要がありますが、その時点ですでに言語のインタープリターを作成しています。
Mehrdad

回答:


29

多くのLispersは、どのようなLispのスペシャルを作ることであることを教えてくれますhomoiconicityコードの構文は、他のデータと同じデータ構造を用いて表現されることを意味し、。たとえば、次は、指定された辺の長さの直角三角形の斜辺を計算するための単純な関数です(Scheme構文を使用)。

(define (hypot x y)
  (sqrt (+ (square x) (square y))))

さて、ホモイコニシティは、上記のコードは実際にはLispコードのデータ構造(具体的にはリストのリスト)として表現可能であると言っています。したがって、次のリストを検討し、それらがどのように「接着」するかを確認してください。

  1. (define #2# #3#)
  2. (hypot x y)
  3. (sqrt #4#)
  4. (+ #5# #6#)
  5. (square x)
  6. (square y)

マクロを使用すると、ソースコードを単なるもののリストとして扱うことができます。これら6「サブリスト」はそれぞれ、他のリストのいずれかのポインタが含まれている、または記号に(この例では:definehypotxysqrt+square)。


では、どのようにホモイコニシティを使用して構文を「選択」し、マクロを作成できますか?以下に簡単な例を示します。letを呼び出すマクロを再実装しましょうmy-let。念のため、

(my-let ((foo 1)
         (bar 2))
  (+ foo bar))

に展開する必要があります

((lambda (foo bar)
   (+ foo bar))
 1 2)

以下に、Schemeの「明示的な名前変更」マクロを使用した実装を示します

(define-syntax my-let
  (er-macro-transformer
    (lambda (form rename compare)
      (define bindings (cadr form))
      (define body (cddr form))
      `((,(rename 'lambda) ,(map car bindings)
          ,@body)
        ,@(map cadr bindings)))))

formパラメータは、実際のフォームにバインドされているので、この例のために、それは次のようになります(my-let ((foo 1) (bar 2)) (+ foo bar))。それでは、例を見ていきましょう。

  1. まず、フォームからバインディングを取得します。フォームcadr((foo 1) (bar 2))一部を取得します。
  2. 次に、フォームから本文を取得します。フォームcddr((+ foo bar))一部を取得します。(これは、バインディング後のすべてのサブフォームを取得することを目的としていることに注意してください。

    (my-let ((foo 1)
             (bar 2))
      (debug foo)
      (debug bar)
      (+ foo bar))
    

    その後、体はなります((debug foo) (debug bar) (+ foo bar))。)

  3. ここで、結果のlambda式を実際に構築し、収集したバインディングと本体を使用して呼び出します。バックティックは「準クォート」と呼ばれます。これは、コンマの後のビットを除く、準クォート内のすべてをリテラルデータムとして扱うことを意味します(「アンクォート」)。
    • (rename 'lambda)手段が使用するlambdaこのマクロがされたときに力で結合を定義し、むしろ何よりもlambdaこのマクロがされたときに周りのかもしれませんバインディングを使用します。(これは衛生として知られています。)
    • (map car bindings)戻り値(foo bar):各バインディングの最初のデータ。
    • (map cadr bindings)戻り値(1 2):各バインディングの2番目のデータ。
    • ,@ リストを返す式に使用される「スプライシング」を行います。リスト自体ではなく、リストの要素を結果に貼り付けます。
  4. これらすべてをまとめると、結果としてlist (($lambda (foo bar) (+ foo bar)) 1 2)が得られ$lambdaますlambda。ここで、renamedを指します。

簡単ですよね?;-)(あなたにとって簡単でない場合は、他の言語のマクロシステムを実装することがどれほど難しいか想像してください。)


したがって、ソースコードを不格好な方法で「分離」できる方法があれば、他の言語のマクロシステムを使用できます。これにはいくつかの試みがあります。たとえば、sweet.jsはJavaScriptに対してこれを行います。

†これを読んでいる熟練したSchemer defmacroのために、他のLisp方言で使用されるsの間の妥協案として明示的な名前変更マクロを使用することを意図的に選択しましたsyntax-rules。私は他のLisp方言で書きたくありませんが、慣れていない非Schemersを疎外したくありませんsyntax-rules

参考のために、次のmy-letマクロを使用しますsyntax-rules

(define-syntax my-let
  (syntax-rules ()
    ((my-let ((id val) ...)
       body ...)
     ((lambda (id ...)
        body ...)
      val ...))))

対応するsyntax-caseバージョンは非常に似ています:

(define-syntax my-let
  (lambda (stx)
    (syntax-case stx ()
      ((_ ((id val) ...)
         body ...)
       #'((lambda (id ...)
            body ...)
          val ...)))))

2つの違いは、のすべてsyntax-rules暗黙的な#'適用があるため、でパターンとテンプレートのペアのみを持つことができるためsyntax-rules、完全に宣言的であるということです。対照的に、syntax-caseでは、パターンの後のビットは実際にはコードであり、最終的には構文オブジェクト(#'(...))を返す必要がありますが、他のコードも含めることができます。


2
言及していない利点:はい、JSのsweet.jsなど、他の言語での試みがあります。ただし、lispsでは、マクロの記述は関数の記述と同じ言語で行われます。
フロリアンマーゲイン

確かに、手続き型(宣言型に対して)マクロをLisp言語で記述できます。これにより、本当に高度なことを行うことができます。ところで、これはSchemeマクロシステムについて私が好きなことです:複数の選択肢があります。単純なマクロにはsyntax-rules、純粋に宣言的なを使用します。複雑なマクロの場合、を使用できますsyntax-case。これは、部分的に宣言的で部分的に手続き的です。そして、明示的な名前変更がありますが、これは純粋に手続き型です。(ほとんどのScheme実装は、どちらかsyntax-caseまたはERを提供します。両方を提供するものは見当たりません。それらは同等です。)
クリスジェスターヤング

マクロがASTを変更する必要があるのはなぜですか?なぜ彼らはより高いレベルで働けないのですか?
エリオットゴロホフスキー

1
それでは、なぜLISPが優れているのでしょうか?LISPが特別な理由は何ですか?jsでマクロを実装できる場合、他の言語でもマクロを実装できます。
エリオットゴロホフスキー

3
最初のコメントで述べたように、@RenéGの大きな利点は、あなたがまだ同じ言語で書いているということです。
フロリアンマーゲイン

23

反対意見:Lispのホモイコニシティは、ほとんどのLispファンがあなたに信じさせるよりもはるかに有用なものではありません。

構文マクロを理解するには、コンパイラを理解することが重要です。コンパイラの仕事は、人間が読めるコードを実行可能なコードに変換することです。非常に高レベルの観点から、これには2つの全体的なフェーズがあります解析コード生成

解析とは、コードを読み取り、一連の正式な規則に従って解釈し、一般にAST(Abstract Syntax Tree)として知られるツリー構造に変換するプロセスです。プログラミング言語間のすべての多様性について、これは1つの注目すべき共通点です。本質的にすべての汎用プログラミング言語は、ツリー構造に解析されます。

コード生成は、パーサーのASTを入力として受け取り、正式なルールの適用を介して実行可能コードに変換します。パフォーマンスの観点から、これははるかに簡単なタスクです。多くの高レベル言語コンパイラは、解析に75%以上の時間を費やしています。

Lispについて覚えておくべきことは、それが非常に古いということです。プログラミング言語の中で、FORTRANのみがLispよりも古いです。昔のことですが、構文解析(コンパイルの遅い部分)は暗くて神秘的な芸術と考えられていました。ジョンマッカーシーのLispの理論に関する元の論文(実際のコンピュータープログラミング言語として実際に実装できるとは考えていなかったという考えでした)は、現代の「S式すべてよりも複雑で表現力豊かな構文」について説明しています。 「表記。それは後で人々が実際にそれを実装しようとしていたときに起こりました。当時は構文解析が十分に理解されていなかったため、パーサーの仕事を非常に些細なものにするために、基本的に構文解析をホモティックなツリー構造に落とし込みました。最終結果は、あなた(開発者)が多くのパーサーをしなければならないということです。■正式なASTをコードに直接書き込むことで機能します。ホモイコニシティは、他のすべてを非常に難しくするほど「マクロをそれほど簡単にする」ことはありません!

これに伴う問題は、特に動的型付けでは、S式が多くの意味情報を持ち運ぶことは非常に難しいことです。すべての構文が同じタイプ(リストのリスト)である場合、構文によって提供されるコンテキストの方法はあまりないため、マクロシステムはほとんど機能しません。

コンパイラ理論は、Lispが発明された1960年代以来長い道のりを歩んできました。Lispが達成したことは当時印象的でしたが、今ではかなり原始的に見えます。最新のメタプログラミングシステムの例については、(残念ながら過小評価されている)Boo言語をご覧ください。Booは静的に型付けされ、オブジェクト指向で、オープンソースであるため、すべてのASTノードには、マクロ開発者がコードを読み取ることができる明確に定義された構造を持つ型があります。この言語は、Pythonに触発された比較的単純な構文を持ち、それらから構築されたツリー構造に固有の意味を与えるさまざまなキーワードを使用します。

昨日作成したマクロは、GUIコードのさまざまな場所に同じパターンを適用していることに気付いたときに作成しBeginUpdate()ました。UIコントロールを呼び出し、tryブロックで更新を実行してから呼び出しますEndUpdate()

macro UIUpdate(value as Expression):
    return [|
        $value.BeginUpdate()
        try:
            $(UIUpdate.Body)
        ensure:
            $value.EndUpdate()
    |]

macroコマンドは、実際には、あるマクロ自体、入力としてマクロボディを受け取り、マクロを処理するためのクラスを生成する1。MacroStatementマクロの呼び出しを表すASTノードを表す変数として、マクロの名前を使用します。[| ... |]は、準引用ブロックであり、準引用ブロック内およびその内部のコードに対応するASTを生成します。$記号は、指定されたノードで置換する「引用解除」機能を提供します。

これにより、次のように書くことができます。

UIUpdate myComboBox:
   LoadDataInto(myComboBox)
   myComboBox.SelectedIndex = 0

それを次のように展開します。

myComboBox.BeginUpdate()
try:
   LoadDataInto(myComboBox)
   myComboBox.SelectedIndex = 0
ensure:
   myComboBox.EndUpdate()

この方法でマクロを表現するのは、Lispマクロの場合よりも単純で直感的です。開発者は構造とプロパティMacroStatementがどのように機能するかを知っており、固有の知識を使用して非常に直感的な概念を表現できるため方法。コンパイラはの構造を知っているため、より安全です。aに対して無効なものをコーディングしようとすると、コンパイラはすぐにそれをキャッチし、何かが爆発するまでわからない代わりにエラーを報告します。ランタイム。ArgumentsBodyMacroStatementMacroStatement

Haskell、Python、Java、Scalaなどにマクロを移植することは難しくありません。これらの言語は同型ではないためです。言語はそれらのために設計されていないため困難であり、言語のAST階層がマクロシステムによって検査および操作されるように一から設計されている場合に最適に機能します。最初からメタプログラミングを念頭に置いて設計された言語を使用している場合、マクロははるかにシンプルで簡単に使用できます。


4
読んでよかった、ありがとう!非Lispマクロは構文を変更する限り拡張しますか?Lispの強みの1つは構文がすべて同じであるため、すべて同じであるために関数、条件付きステートメントを追加するのは簡単です。非Lisp言語では、1つのことが別のものとは異なりif...ますが、たとえば関数呼び出しのようには見えません。Booを知りませんが、Booにパターンマッチングがなかったと想像してください。マクロとしての独自の構文を使用してBooを紹介できますか。私のポイントは、Lispの新しいマクロは100%自然で、他の言語では機能するが、ステッチが見えることです。
greenoldman

4
私がいつも読んだ物語は少し異なっています。s-expressionの代替構文が計画されましたが、プログラマーがすでにs-expressionを使い始めて便利だと判断したため、作業が遅れました。そのため、新しい構文の作業は最終的に忘れられました。S式を使用する理由としてコンパイラ理論の欠点を示すソースを引用してください。また、Lispファミリーは何十年も進化し続けており(Scheme、Common Lisp、Clojure)、ほとんどの方言はs式に固執することを決めました。
ジョルジオ

5
「よりシンプルで直感的」:申し訳ありませんが、方法がわかりません。「Updating.Arguments [0]」ではない意味のある、私はむしろ名前付き引数を持っていると引数の一致の数ならば、コンパイラのチェックitselfsを聞かせたい:pastebin.com/YtUf1FpG
コアダンプ

8
「パフォーマンスの観点から、これははるかに単純なタスクです。多くの高レベル言語コンパイラは、解析に75%以上の時間を費やしています。」ほとんどの時間を費やすために最適化を探して適用することを期待していました(ただし、実際のコンパイラを作成したことはありません)。ここに何かが足りませんか?
ドーバル

5
残念ながら、あなたの例はそれを示していません。マクロを使用して任意のLispに実装するのはプリミティブです。実際、これは実装する最も原始的なマクロの1つです。これは、あなたがLispのマクロについてあまり知らないのではないかと疑っています。「Lispの構文は1960年代に行き詰まっています」:実際には、Lispのマクロシステムは1960年以来多くの進歩を遂げています(1960年にはLispにもマクロはありませんでした!)。
レイナージョスヴィッヒ

3

私はSICPからSchemeを学んでいますが、Schemeを作るものの大きな部分、さらにLISPが特別なのはマクロシステムであるという印象を受けています。

どうして?SICPのすべてのコードは、マクロを使用しないスタイルで記述されています。SICPにはマクロはありません。マクロは、373ページの脚注でのみ言及されています。

ただし、マクロはコンパイル時に展開されるため

必ずしもそうではありません。Lispは、インタープリターとコンパイラーの両方でマクロを提供します。したがって、コンパイル時がないかもしれません。Lispインタプリタがある場合、実行時にマクロが展開されます。多くのLispシステムにはコンパイラが搭載されているため、実行時にコードを生成してコンパイルできます。

Common Lispの実装であるSBCLを使用して、テストしてみましょう。

SBCLをインタープリターに切り替えましょう:

* (setf sb-ext:*evaluator-mode* :interpret)

:INTERPRET

次に、マクロを定義します。マクロは、コードを展開するために呼び出されると何かを出力します。生成されたコードは印刷されません。

* (defmacro my-and (a b)
    (print "macro my-and used")
    `(if ,a
         (if ,b t nil)
         nil))

次に、マクロを使用しましょう。

MY-AND
* (defun foo (a b) (my-and a b))

FOO

見る。上記の場合、Lispは何もしません。マクロは定義時に展開されません。

* (foo t nil)

"macro my-and used"
NIL

ただし、実行時にコードが使用されると、マクロが展開されます。

* (foo t t)

"macro my-and used"
T

繰り返しますが、実行時に、コードが使用されると、マクロが展開されます。

コンパイラを使用する場合、SBCLは一度しか展開しないことに注意してください。しかし、さまざまなLisp実装は、SBCLなどのインタープリターも提供します。

Lispでマクロが簡単なのはなぜですか?まあ、彼らは本当に簡単ではありません。Lispにのみ、マクロサポートが組み込まれたものが多数あります。多くのLispにはマクロ用の広範な機構が付属しているため、簡単に見えます。しかし、マクロのメカニズムは非常に複雑になる可能性があります。


私はSICPを読んでいるだけでなく、ウェブ上のSchemeについてたくさん読んでいます。また、解釈される前にLisp式はコンパイルされませんか?少なくとも解析する必要があります。したがって、「コンパイル時間」は「解析時間」でなければなりません。
エリオットゴロホフスキー

@RenéGRainerのポイントは、あなたevalloadLisp言語でコーディングすると、それらのマクロも処理されるということだと思います。一方、質問で提案されているプリプロセッサシステムを使用する場合evalなどは、マクロ展開の恩恵を受けません。
クリスジェスターヤング

@RenéGまた、「解析」はreadLisp で呼び出されます。この区別は重要です。なぜならeval、テキスト形式ではなく、実際のリストデータ構造(私の回答で述べたとおり)で機能するからです。したがって(eval '(+ 1 1))、2を使用して戻すことができますが、2を使用すると、戻り(eval "(+ 1 1)")ます"(+ 1 1)"(文字列)。(7文字の文字列)から(1つのシンボルと2つのfixnumのリスト)readに取得するために使用します。"(+ 1 1)"(+ 1 1)
クリスジェスターヤング

@RenéGその理解では、マクロはread-timeで機能しません。のようなコードがある場合、コードが実行されるたびにではなく、コードがロードされるときに一度だけ(Schemeで)(and (test1) (test2))展開されるという意味でコンパイル時に動作します(if (test1) (test2) #f)が、(eval '(and (test1) (test2)))、実行時にその式を適切にコンパイル(およびマクロ展開)します。
クリスジェスターヤング

@RenéGHomoiconicityは、Lisp言語がテキスト形式ではなくリスト構造を評価し、それらのリスト構造を実行前に(マクロを介して)変換できるようにするものです。ほとんどの言語evalはテキスト文字列のみで動作し、構文変更の機能はより光沢がなく、面倒です。
クリスジェスターヤング

1

同相性により、マクロの実装がはるかに容易になります。コードはデータであり、データはコードであるという考えにより、多かれ少なかれ(識別子の誤ったキャプチャを除き、衛生的なマクロによって解決されます)、一方を他方に自由に置き換えることができます。LispとSchemeは、均一に構造化されたS式の構文によりこれを容易にし、Syntactic Macrosの基礎を形成するASTに簡単に変換できます

S-ExpressionsやHomoiconicityのない言語では、構文マクロの実装に問題が生じますが、それでも確実に実行できます。例えば、プロジェクトケプラーはそれらをScalaに紹介しようとしています。

非同次性以外の構文マクロの使用に関する最大の問題は、任意に生成された構文の問題です。これらは非常に高い柔軟性とパワーを提供しますが、ソースコードが理解や保守が簡単ではなくなるかもしれないという代償を払っています。

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