web-dev-qa-db-ja.com

LISPマクロが特別な理由は何ですか?

ポールグラハムのエッセイ プログラミング言語では、 LISPマクロ が唯一の方法だと思うでしょう。他のプラットフォームで作業する忙しい開発者として、私はLISPマクロを使用する特権を持っていません。バズを理解したい人として、この機能が非常に強力な理由を説明してください。

これを、Python、Java、C#、またはC開発の世界から理解できるものに関連付けてください。

282
minty

簡潔な答えを出すために、Common LISPまたはドメイン固有言語(DSL)の言語構文拡張を定義するためにマクロが使用されます。これらの言語は、既存のLISPコードに直接埋め込まれています。現在、DSLには、LISPに類似した構文(Peter Norvigの Prolog Interpreter Common LISPのような)または完全に異なる構文(例: Infix Notation Math Clojure)があります。

より具体的な例を次に示します。
Pythonには、言語に組み込まれたリスト内包表記があります。これにより、一般的なケースの単純な構文が得られます。この線

divisibleByTwo = [x for x in range(10) if x % 2 == 0]

0〜9のすべての偶数を含むリストを生成します。 Python 1.5 に戻り、そのような構文はありませんでした。次のようなものを使用します。

divisibleByTwo = []
for x in range( 10 ):
   if x % 2 == 0:
      divisibleByTwo.append( x )

これらは両方とも機能的に同等です。不信の停止を呼び出してみましょう。LISPには非常に限定されたループマクロがあり、反復を行うだけで、リスト内包に相当する簡単な方法はありません。

LISPでは、次のように書くことができます。この不自然な例は、Pythonコードと同一であるように選択されており、LISPコードの良い例ではないことに注意してください。

;; the following two functions just make equivalent of Python's range function
;; you can safely ignore them unless you are running this code
(defun range-helper (x)
  (if (= x 0)
      (list x)
      (cons x (range-helper (- x 1)))))

(defun range (x)
  (reverse (range-helper (- x 1))))

;; equivalent to the python example:
;; define a variable
(defvar divisibleByTwo nil)

;; loop from 0 upto and including 9
(loop for x in (range 10)
   ;; test for divisibility by two
   if (= (mod x 2) 0) 
   ;; append to the list
   do (setq divisibleByTwo (append divisibleByTwo (list x))))

先に進む前に、マクロとは何かをよりよく説明する必要があります。コードで実行される変換です 沿って コード。つまり、インタープリター(またはコンパイラー)が読み取り、引数としてコードを受け取り、結果を操作して返すコードの一部は、インプレースで実行されます。

もちろん、それは多くのタイピングであり、プログラマーは怠け者です。したがって、リストを理解するためにDSLを定義できます。実際、既に1つのマクロ(ループマクロ)を使用しています。

LISPは、いくつかの特別な構文形式を定義しています。引用符(')は、次のトークンがリテラルであることを示します。準引用符またはバックティック(`)は、次のトークンがエスケープ付きのリテラルであることを示します。エスケープはコンマ演算子で示されます。リテラル'(1 2 3)は、Pythonの[1, 2, 3]と同等です。別の変数に割り当てるか、その場で使用できます。 `(1 2 ,x)はPythonの[1, 2, x]と同等と考えることができます。ここで、xは以前に定義された変数です。このリスト表記は、マクロに入る魔法の一部です。 2番目の部分はLISPリーダーで、コードの代わりにインテリジェントにマクロを使用しますが、以下に最もよく示されています。

したがって、lcomp(リスト内包表記の略)というマクロを定義できます。構文は、例で使用したpythonとまったく同じです[x for x in range(10) if x % 2 == 0]-(lcomp x for x in (range 10) if (= (% x 2) 0))

(defmacro lcomp (expression for var in list conditional conditional-test)
  ;; create a unique variable name for the result
  (let ((result (gensym)))
    ;; the arguments are really code so we can substitute them 
    ;; store nil in the unique variable name generated above
    `(let ((,result nil))
       ;; var is a variable name
       ;; list is the list literal we are suppose to iterate over
       (loop for ,var in ,list
            ;; conditional is if or unless
            ;; conditional-test is (= (mod x 2) 0) in our examples
            ,conditional ,conditional-test
            ;; and this is the action from the earlier LISP example
            ;; result = result + [x] in python
            do (setq ,result (append ,result (list ,expression))))
           ;; return the result 
       ,result)))

これで、コマンドラインで実行できます。

CL-USER> (lcomp x for x in (range 10) if (= (mod x 2) 0))
(0 2 4 6 8)

きれいですね。これで終わりではありません。必要に応じて、メカニズムまたはペイントブラシを使用できます。必要に応じて任意の構文を使用できます。 PythonまたはC#のwith構文と同様です。または.NETのLINQ構文。最終的に、これが人々をLISPに引き付けるものであり、究極の柔軟性です。

288
gte525u

LISPマクロはこちら について包括的な議論があります。

その記事の興味深いサブセット:

ほとんどのプログラミング言語では、構文は複雑です。マクロは、プログラムの構文を分解して分析し、再構築する必要があります。プログラムのパーサーにアクセスできないため、ヒューリスティックと最良の推測に依存する必要があります。カット率の分析が間違っている場合があり、それから壊れます。

しかし、LISPは異なります。 LISPマクロdoはパーサーにアクセスできます。これは非常に単純なパーサーです。 LISPマクロには文字列は渡されませんが、LISPプログラムのソースは文字列ではないため、リストの形式で事前解析されたソースコードの一部です。それはリストです。そして、LISPプログラムはリストを分解して元に戻すのが得意です。彼らは毎日これを確実に行います。

以下に拡張例を示します。 LISPには、割り当てを実行する「setf」と呼ばれるマクロがあります。 setfの最も単純な形式は

  (setf x whatever)

シンボル「x」の値を式「whatever」の値に設定します。

LISPにはリストもあります。 「car」関数と「cdr」関数を使用して、リストの最初の要素またはリストの残りの要素をそれぞれ取得できます。

リストの最初の要素を新しい値で置き換えたい場合はどうでしょうか?それを行うための標準関数があり、信じられないほど、その名前は「車」よりもさらに悪いです。 「rplaca」です。ただし、「rplaca」を覚える必要はありません。

  (setf (car somelist) whatever)

somelistの車を設定します。

ここで実際に行われているのは、「setf」がマクロであることです。コンパイル時に、引数を調べ、最初の引数の形式(car SOMETHING)があることを確認します。 「ああ、プログラマは気の毒な車を設定しようとしています。そのために使用する関数は「rplaca」です。」そして、コードを次のように静かに書き換えます。

  (rplaca somelist whatever)
102
VonC

一般的なLISPマクロは、基本的にコードの「構文プリミティブ」を拡張します。

たとえば、Cでは、switch/caseコンストラクトは整数型でのみ機能し、floatまたは文字列に使用する場合、ネストされたifステートメントと明示的な比較が残ります。また、Cマクロを作成してジョブを実行する方法もありません。

しかし、LISPマクロは(本質的に)コードのスニペットを入力として受け取り、マクロの「呼び出し」を置き換えるコードを返すLISPプログラムであるため、「プリミティブ」レパートリーを必要に応じて拡張することができます。より読みやすいプログラムで。

Cで同じことを行うには、最初の(まったくCではない)ソースを食べ、Cコンパイラが理解できるものを吐き出すカスタムプリプロセッサを記述する必要があります。それについては間違った方法ではありませんが、必ずしも最も簡単な方法ではありません。

53
Vatine

LISPマクロを使用すると、(あるとしても)部分または式をいつ評価するかを決定できます。簡単な例を挙げると、Cのことを考えてください。

expr1 && expr2 && expr3 ...

これは、expr1を評価し、それが本当であれば、expr2などを評価します。

ここで、この&&を関数にしようとします...そうです、できません。次のようなものを呼び出す:

and(expr1, expr2, expr3)

expr1がfalseであったかどうかに関係なく、3つのexprsすべてを評価してから答えを出します。

LISPマクロを使用すると、次のようなコードを記述できます。

(defmacro && (expr1 &rest exprs)
    `(if ,expr1                     ;` Warning: I have not tested
         (&& ,@exprs)               ;   this and might be wrong!
         nil))

これで&&ができました。これは関数のように呼び出すことができ、すべて真である場合を除き、渡したフォームを評価しません。

これがどのように役立つかを見るために、対照的に:

(&& (very-cheap-operation)
    (very-expensive-operation)
    (operation-with-serious-side-effects))

そして:

and(very_cheap_operation(),
    very_expensive_operation(),
    operation_with_serious_side_effects());

マクロでできる他のことは、新しいキーワードやミニ言語を作成することです(例については (loop ...) マクロを確認してください)。他の言語をLISPに統合します。たとえば、次のように言うことができます:

(setvar *rows* (sql select count(*)
                      from some-table
                     where column1 = "Yes"
                       and column2 like "some%string%")

そして、それは Readerマクロ にさえ入らない。

お役に立てれば。

39
dsm

私は、LISPマクロがこの仲間よりもよく説明されているのを見たことがないと思います。 http://www.defmacro.org/ramblings/LISP.html

29
Rayne

LISPマクロは、プログラムフラグメントを入力として受け取ります。このプログラムフラグメントは、任意の方法で操作および変換できるデータ構造を表しています。最後に、マクロは別のプログラムフラグメントを出力し、このフラグメントは実行時に実行されます。

C#にはマクロ機能はありませんが、コンパイラがコードをCodeDOMツリーに解析し、それをメソッドに渡し、これを別のCodeDOMに変換し、それをILにコンパイルする場合に相当します。

これは、for each- statement using- clause、linq select- expressionsなどの「シュガー」構文を、基礎となるコードに変換するマクロとして実装するために使用できます。

Javaにマクロがある場合、ベース言語を変更するためにSunを必要とせずに、JavaでLinq構文を実装できます。

usingを実装するためのC#のLISPスタイルマクロがどのように見えるかを示す擬似コードは次のとおりです。

define macro "using":
    using ($type $varname = $expression) $block
into:
    $type $varname;
    try {
       $varname = $expression;
       $block;
    } finally {
       $varname.Dispose();
    }
10
JacquesB

マクロやテンプレートを使用して、CまたはC++でできることを考えてください。繰り返しコードを管理するための非常に便利なツールですが、非常に厳しい方法で制限されています。

  • 制限されたマクロ/テンプレート構文は、それらの使用を制限します。たとえば、クラスまたは関数以外に展開するテンプレートを作成することはできません。マクロとテンプレートは、内部データを簡単に維持できません。
  • CおよびC++の複雑で非常に不規則な構文により、非常に一般的なマクロを記述することは困難です。

LISPおよびLISPマクロはこれらの問題を解決します。

  • LISPマクロはLISPで記述されています。マクロを作成するためのLISPのフルパワーがあります。
  • LISPの構文は非常に規則的です。

C++をマスターした人と話して、テンプレートメタプログラミングを行うために必要なすべてのテンプレートファッジリーの学習にどれだけの時間を費やしたかを尋ねます。または、Modern C++ Designのような(優れた)本のすべてのクレイジーなトリックは、言語が10年。メタプログラミングに使用する言語がプログラミングに使用する言語と同じである場合、そのすべては溶けてしまいます!

9
Matt Curtis

みんなの(優秀な)投稿に洞察を加えることができるかどうかはわかりませんが、...

LISP構文の性質により、LISPマクロは素晴らしい働きをします。

LISPは非常に規則的な言語です(すべての考えはlist) ;マクロを使用すると、データとコードを同じものとして扱うことができます(LISP式を変更するために文字列解析やその他のハックは必要ありません)。これら2つの機能を組み合わせて、コードを修正する非常にcleanの方法があります。

編集:私が言おうとしていたことは、LISPは homoiconic であり、LISPプログラムのデータ構造はLISP自体で書かれています。

そのため、言語自体を最大限に活用して、言語の上に独自のコードジェネレーターを作成する方法になります(たとえば、Javaでは、バイトコードの織り方でハックする必要がありますが、 AspectJでは、異なるアプローチを使用してこれを行うことができます。これは基本的にハックです)。

実際には、マクロを使用すると、LISPの上に独自のミニ言語を構築することになり、追加の言語やツールを学習する必要がなく、言語の全機能を使用できます自体。

8
Miguel Ping

既存の回答はマクロが達成することとその方法を説明する良い具体例を提供しているので、おそらく、マクロ機能が他の言語と比較して重要な利得である理由についての考えをまとめるのに役立つでしょう;最初にこれらの回答から、次に他の場所からの素晴らしい回答:

... Cでは、カスタムプリプロセッサを記述する必要があります[おそらく、 十分に複雑なCプログラム ]として認定されます...

Vatine

C++をマスターした人と話して、テンプレートメタプログラミングを行うために必要なすべてのテンプレートファッジリーを学習するのにどれくらいの時間を費やしたかを尋ねます(これはまだ強力ではありません)。

マットカーティス

... Javaでは、バイトコード織りでハックする必要がありますが、AspectJのような一部のフレームワークでは別のアプローチを使用してこれを行うことができますが、基本的にはハックです。

ミゲルピン

DOLISTはPerlのforeachまたはPythonのforeachに似ています。 Javaは、JSR-201の一部として、Java 1.5の「強化された」forループと同様の種類のループ構造を追加しました。マクロの違いに注目してください。コードに共通のパターンがあることに気付いたLISPプログラマは、マクロを記述して、そのパターンのソースレベルの抽象化を自分で行うことができます。同じパターンに気づいたJavaプログラマは、この特定の抽象化が言語に追加する価値があることをSunに納得させる必要があります。次に、SunはJSRを発行し、業界全体の「専門家グループ」を招集してすべてをハッシュ化する必要があります。 Sunによると、このプロセスには平均18か月かかります。その後、コンパイラの作成者はすべて、コンパイラをアップグレードして新しい機能をサポートする必要があります。そして、Javaプログラマーのお気に入りのコンパイラがJavaの新しいバージョンをサポートしたとしても、Javaの古いバージョンとのソース互換性を破るまで、「まだ」新しい機能を使用できないでしょう。そのため、Common LISPプログラマーが5分以内に自分で解決できるという悩みは、長年Javaプログラマーを悩ませています。

「Practal Common LISP」のPeter Seibel

8
ZakW

LISPマクロは、ほとんどすべての大規模なプログラミングプロジェクトで発生するパターンを表します。最終的に大規模なプログラムでは、ソースコードをテキストとして出力するプログラムを作成し、それを貼り付けるだけでエラーが発生しにくくなるという特定のコードセクションがあります。

Pythonオブジェクトには、2つのメソッド__repr__および__str__があります。 __str__は、単に人間が読める形式です。 __repr__は有効なPythonコードである表現を返します。つまり、有効なPythonとしてインタープリターに入力できるものです。この方法で、実際のソースに貼り付けることができる有効なコードを生成するPythonの小さなスニペットを作成できます。

LISPでは、このプロセス全体がマクロシステムによって形式化されています。確かに、構文の拡張機能を作成し、あらゆる種類の凝ったことを行うことができますが、実際の有用性は上記によってまとめられています。もちろん、LISPマクロシステムを使用すると、これらの「スニペット」を言語全体のフルパワーで操作できるようになります。

6
dnolen

要するに、マクロはコードの変換です。多くの新しい構文構成を導入できます。たとえば、C#のLINQを検討します。 LISPには、マクロによって実装される類似の言語拡張機能があります(たとえば、組み込みのループ構造、反復)。マクロは、コードの重複を大幅に減らします。マクロを使用すると、"小さな言語"(たとえば、c#/ Javaではxmlを使用して構成しますが、LISPではマクロで同じことを実現できます)を埋め込むことができます。マクロを使用すると、ライブラリを使用する際の困難を隠すことができます。

たとえば、LISPでは次のように記述できます

(iter (for (id name) in-clsql-query "select id, name from users" on-database *users-database*)
      (format t "User with ID of ~A has name ~A.~%" id name))

これにより、すべてのデータベース(トランザクション、適切な接続のクローズ、データのフェッチなど)が非表示になりますが、C#では、SqlConnections、SqlCommandsの作成、SqlParametersのSqlCommandsへの追加、SqlDataReadersでのループ、それらの適切なクローズが必要です。

5
dmitry_vk

上記はすべてマクロとは何かを説明し、すばらしい例さえありますが、マクロと通常の関数の主な違いは、関数を呼び出す前にLISPがすべてのパラメーターを最初に評価することだと思います。マクロの場合は逆になり、LISPは評価されていないパラメーターをマクロに渡します。たとえば、関数に(+ 1 2)を渡すと、関数は値3を受け取ります。これをマクロに渡すと、List(+ 1 2)を受け取ります。これは、あらゆる種類の非常に便利なことを行うために使用できます。

  • 新しい制御構造の追加、例:ループまたはリストの分解
  • 渡された関数の実行にかかる時間を測定します。関数を使用すると、制御が関数に渡される前にパラメーターが評価されます。マクロを使用すると、ストップウォッチの開始と停止の間でコードを継ぎ合わせることができます。以下は、マクロと関数でまったく同じコードを持ち、出力は非常に異なります。 注:これは人為的な例であり、違いをより強調するために同一になるように実装が選択されました。

    (defmacro working-timer (b) 
      (let (
            (start (get-universal-time))
            (result (eval b))) ;; not splicing here to keep stuff simple
        ((- (get-universal-time) start))))
    
    (defun my-broken-timer (b)
      (let (
            (start (get-universal-time))
            (result (eval b)))    ;; doesn't even need eval
        ((- (get-universal-time) start))))
    
    (working-timer (sleep 10)) => 10
    
    (broken-timer (sleep 10)) => 0
    
2

私はこれをThe Common LISP cookbookから入手しましたが、LISPマクロが素晴らしい点で良い理由を説明したと思います。

「マクロは、別の推定LISPコードで動作する通常のLISPコードであり、実行可能なLISP(に近いバージョン)に変換します。少し複雑に聞こえるかもしれないので、簡単な例を示します。 2つの変数を同じ値に設定するsetqのバージョンです。

(setq2 x y (+ z 3))

z=8 xとyの両方が11に設定されている場合(これを使用することは考えられませんが、これは単なる例です)。

Setq2を関数として定義できないことは明らかです。 x=50およびy=-5の場合、この関数は値50、-5、および11を受け取ります。どの変数が設定されることになっていたのかはわかりません。本当に言いたいのは、あなた(LISPシステム)が(setq2 v1 v2 e)を見るとき、それを(progn (setq v1 e) (setq v2 e))と同等のものとして扱うことです。実際には、これはまったく正しくありませんが、今のところはうまくいきます。マクロを使用すると、入力パターン(setq2 v1 v2 e) "を出力パターン(progn ...)。"に変換するプログラムを指定することにより、これを正確に行うことができます。

これがいいと思ったら、ここで読み続けることができます: http://cl-cookbook.sourceforge.net/macros.html

0
nilsi