web-dev-qa-db-ja.com

LISPについては、マクロシステムの実装が容易になるとしたらどうでしょうか。

私は[〜#〜] sicp [〜#〜]からSchemeを学習しています。Schemeを構成する要素の大部分、さらにはLISPが特別であるという印象を受けています。マクロシステム。しかし、マクロはコンパイル時に展開されるので、人々がC/Python/Java /その他の同等のマクロシステムを作成しないのはなぜですか。たとえば、pythonコマンドをexpand-macros | pythonなどにバインドできます。コードは、マクロシステムを使用しないユーザーにも移植可能です。コードを公開する前にマクロを展開するだけです。しかし、C++/Haskellのテンプレートを除いて、そのようなものは何も知りません。 LISPについては、マクロシステムの実装が容易になるとしたらどうでしょうか。

21

多くの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式を実際に作成し、収集したバインディングと本体を使用して呼び出します。バックティックは「quasiquote」と呼ばれ、quasiquote内のすべてをリテラルデータムとして扱いますexceptコンマの後のビット( "unquote")。
    • _(rename 'lambda)_は、このマクロがsedのときにlambdaバインディングが存在する可能性があるのではなく、このマクロがdefinedのときに強制的にlambdaバインディングを使用することを意味します。 (これはhygieneとして知られています。)
    • _(map car bindings)_は_(foo bar)_を返します。各バインディングの最初のデータムです。
    • _(map cadr bindings)_は_(1 2)_を返します。各バインディングの2番目のデータムです。
    • _,@_は「スプライシング」を行います。これは、リストを返す式に使用されます。これにより、リスト自体ではなく、リストの要素が結果に貼り付けられます。
  4. これらすべてをまとめると、結果として、リスト_(($lambda (foo bar) (+ foo bar)) 1 2)_が取得されます。ここで、_$lambda_は、名前が変更されたlambdaを参照します。

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


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

†これを読んでいる熟練したSchemerの場合、他のLISP方言で使用されるdefmacrosと_syntax-rules_(Schemeでこのようなマクロを実装する標準的な方法です)の中間的な妥協として、明示的な名前変更マクロを使用することを意図的に選択しました。他のLISP方言で記述したくありませんが、_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_のeverythingには暗黙の_#'_が適用されるため、onlyでパターン/テンプレートのペアを_syntax-rules_、それゆえ完全に宣言的です。対照的に、_syntax-case_では、パターンの後のビットは実際にはコードであり、最終的には構文オブジェクト(#'(...))を返す必要がありますが、他のコードも含めることができます。

29

私は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が多数あります。多くのLispにはマクロ用の大規模な機構が付属しているため、簡単に見えます。しかし、マクロのメカニズムは非常に複雑になる可能性があります。

3
Rainer Joswig

Homoiconicity を使用すると、マクロの実装がはるかに簡単になります。コードはデータであり、データはコードであるという考え方により、多かれ少なかれ(識別子の偶発的な取得を除き、 衛生的なマクロ で解決)、一方を他方に自由に置き換えることができます。 LISPとSchemeは、均一に構造化されたS式の構文を使用してこれをより簡単にし、 構文マクロ の基礎を形成するASTに簡単に変換できます。

S-ExpressionsまたはHomoiconicityのない言語では、構文マクロの実装で問題が発生しますが、確実に実行できます。 Project Kepler は、たとえば、Scalaにそれらを紹介しようとしています。

非ホモニコ性以外の構文マクロの使用に関する最大の問題は、任意に生成された構文の問題です。それらは途方もない柔軟性とパワーを提供しますが、その代償として、ソースコードを理解または維持するのが容易ではなくなる可能性があります。

1
World Engineer