プログラミング言語に関するPaul Grahamのエッセイを読むと、Lispマクロが唯一の方法だと思うでしょう。他のプラットフォームで作業している多忙な開発者として、私はLispマクロを使用する特権を持っていませんでした。話題を理解したい人として、この機能が非常に強力になる理由を説明してください。
これを、Python、Java、C#、またはC開発の世界から理解できるものと関連付けてください。
プログラミング言語に関するPaul Grahamのエッセイを読むと、Lispマクロが唯一の方法だと思うでしょう。他のプラットフォームで作業している多忙な開発者として、私はLispマクロを使用する特権を持っていませんでした。話題を理解したい人として、この機能が非常に強力になる理由を説明してください。
これを、Python、Java、C#、またはC開発の世界から理解できるものと関連付けてください。
回答:
簡単に言えば、マクロはCommon Lispまたはドメイン固有言語(DSL)の言語構文拡張を定義するために使用されます。これらの言語は、既存のLispコードに直接埋め込まれています。現在、DSLの構文はLisp(Peter NorvigのCommon Lisp のProlog Interpreterなど)と同じか、まったく異なる(例:ClojureのInfix Notation Math)ことができます。
以下に、より具体的な例を示します
。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]
where 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に引き付けるものです-究極の柔軟性。
(loop for x from 0 below 10 when (evenp x) collect x)
、ここではより多くの例を。しかし、実際には、ループは「単なるマクロ」です(実際には、少し前にゼロから再実装しました)
その記事の興味深いサブセット:
ほとんどのプログラミング言語では、構文は複雑です。マクロは、プログラムの構文を分解して分析し、再構成する必要があります。彼らはプログラムのパーサーにアクセスできないため、ヒューリスティックスと最良の推測に依存する必要があります。時々彼らのカットレート分析は間違っている、そしてそれから彼らは壊れる。
しかし、Lispは異なります。Lispマクロはパーサーにアクセスできますが、これは本当にシンプルなパーサーです。 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)
Common Lispマクロは基本的に、コードの「構文プリミティブ」を拡張します。
たとえば、Cでは、switch / caseコンストラクトは整数型でのみ機能し、floatまたはstringに使用する場合は、ネストされたifステートメントと明示的な比較が残ります。また、Cマクロを記述してその仕事を行う方法もありません。
しかし、lispマクロは(本質的に)コードのスニペットを入力として受け取り、マクロの「呼び出し」を置き換えるコードを返すlispプログラムであるため、「プリミティブ」レパートリーを必要なだけ拡張して、通常はより読みやすいプログラムで。
Cで同じことを行うには、最初の(完全なCではない)ソースを食べ、Cコンパイラが理解できるものを吐き出すカスタムプリプロセッサを作成する必要があります。これを実行するのは間違った方法ではありませんが、必ずしも最も簡単であるとは限りません。
Lispマクロを使用すると、(もしあれば)どの部分または式を評価するかを決定できます。簡単な例として、Cについて考えてみましょう。
expr1 && expr2 && expr3 ...
これが言うことは:評価expr1
、そして、それが本当なら、評価expr2
、などです。
これ&&
を関数にしてみてください...そうです、できません。次のようなものを呼び出す:
and(expr1, expr2, expr3)
間違っていたexprs
かどうかexpr1
に関係なく、答えを出す前に3つすべてを評価します!
lispマクロを使用すると、次のようなコードを記述できます。
(defmacro && (expr1 &rest exprs)
`(if ,expr1 ;` Warning: I have not tested
(&& ,@exprs) ; this and might be wrong!
nil))
&&
これで、関数のように呼び出すことができるがあり、それらがすべてtrueでない限り、渡されたフォームを評価しません。
これがどのように役立つかを見るために、対照的に:
(&& (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%")
そして、それはリーダーマクロにさえ入りません。
お役に立てれば。
(and ...
は、1つがfalseと評価されるまで式を評価します。false評価によって生成される副作用が発生し、後続の式のみがスキップされることに注意してください。
Lispマクロがこのフェローよりよく説明されているのを見たことがないと思います:http : //www.defmacro.org/ramblings/lisp.html
マクロとテンプレートを使用してCまたはC ++で実行できることを考えてください。これらは反復的なコードを管理するための非常に便利なツールですが、非常に厳しい方法で制限されています。
LispおよびLispマクロはこれらの問題を解決します。
C ++をマスターしている人と話し、テンプレートメタプログラミングを行うために必要なすべてのテンプレートの手間をどれだけの時間学習したかを尋ねます。あるいは、Modern C ++ Designのような(優れた)本のすべてのクレイジーなトリックは、デバッグが難しく、言語は10年間標準化されていますが、実際のコンパイラ間では(実際には)移植できません。メタプログラミングに使用する言語がプログラミングに使用する言語と同じである場合、それらすべてが解消されます。
lispマクロは、プログラムフラグメントを入力として受け取ります。このプログラムフラグメントは、任意の方法で操作および変換できるデータ構造を表しています。最後に、マクロは別のプログラムフラグメントを出力し、このフラグメントは実行時に実行されます。
C#にはマクロ機能はありませんが、コンパイラーがコードを解析してCodeDOMツリーに変換し、それをメソッドに渡して別のCodeDOMに変換し、その後ILにコンパイルした場合も同等です。
これは、for each
-statement using
-clause、linq -expressions などの「糖」構文select
を、基礎となるコードに変換するマクロとして実装するために使用できます。
Javaにマクロがある場合、Sunが基本言語を変更する必要なく、JavaにLinq構文を実装できます。
以下は、実装用のC#のlispスタイルのマクロusing
がどのように見えるかを示す疑似コードです。
define macro "using":
using ($type $varname = $expression) $block
into:
$type $varname;
try {
$varname = $expression;
$block;
} finally {
$varname.Dispose();
}
既存の回答は、マクロが何をどのように達成するかを説明する具体的な例を提供しているため、おそらく、マクロ機能が他の言語に比べて大幅に向上している理由についていくつかの考えをまとめることが役立つでしょう。最初にこれらの答えから、次に他の場所からの素晴らしい答え:
... 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プログラマーを何年も悩ませています。
みんなの(優れた)投稿に洞察を追加できるかどうかはわかりませんが...
Lispマクロは、Lisp構文の性質上、優れた働きをします。
Lispは非常に一般的な言語です(すべてがリストであると考えてください)。マクロを使用すると、データとコードを同じものとして扱うことができます(lisp式を変更するために、文字列の解析やその他のハックは必要ありません)。これら2つの機能を組み合わせて、コードを変更するための非常にクリーンな方法があります。
編集:私が言おうとしていたのは、Lispがホモニコ性であることです。つまり、lispプログラムのデータ構造はlisp自体で記述されています。
したがって、言語自体を最大限に活用して、言語の上に独自のコードジェネレーターを作成する方法になってしまいます(たとえば、Javaではバイトコードウィービングでハックする必要がありますが、AspectJなどの一部のフレームワークでは、別のアプローチを使用してこれを行います、それは基本的にハックです)。
実際には、マクロを使用すると、追加の言語やツールを学習する必要なく、言語自体の能力を最大限に活用して、lispの上に独自のミニ言語を構築することになります。
S-expressions
リストではなくで構成されています。
Lispマクロは、ほとんどすべての大規模なプログラミングプロジェクトで発生するパターンを表します。最終的に大規模なプログラムでは、コードの特定のセクションがあり、ソースコードをテキストとして出力するプログラムを作成して貼り付けるだけの方が簡単でエラーが発生しにくいことがわかります。
Pythonオブジェクトには2つのメソッド__repr__
とがあり__str__
ます。 __str__
単に人間が読める表現です。 __repr__
有効なPythonコードである表現を返します。つまり、有効なPythonとしてインタープリターに入力できるものです。このようにして、実際のソースに貼り付けることができる有効なコードを生成するPythonの小さなスニペットを作成できます。
Lispでは、このプロセス全体がマクロシステムによって形式化されています。確かにそれはあなたが構文への拡張を作成してあらゆる種類の空想的なことをすることを可能にします、しかしそれは実際の有用性は上記によって要約されます。もちろん、Lispマクロシステムがこれらの「スニペット」を言語全体の能力をフルに活用して操作できるようにするのに役立ちます。
つまり、マクロはコードの変換です。それらは多くの新しい構文構造を導入することを可能にします。たとえば、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でループして、適切にクローズする必要があります。
上記はすべてマクロが何であるかを説明していて、クールな例さえありますが、マクロと通常の関数の主な違いは、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
これは一般的なlispクックブックから入手しましたが、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
setq2
場合、関数としてx
およびy
参照によって渡されます。CLでそれが可能かどうかはわかりません。したがって、LispやCLを特に知らない人にとっては、これは非常に例証的な例ではありませんIMO
Pythonにはデコレーターがあり、基本的には別の関数を入力として受け取る関数があります。あなたは好きなことをすることができます:関数を呼び出す、何か他のことをする、リソース取得リリースで関数呼び出しをラップするなどですが、その関数の内部を覗くことはできません。より強力にしたいとします。たとえば、デコレータが関数のコードをリストとして受け取ったとすると、関数をそのまま実行できるだけでなく、その一部を実行したり、関数の行を並べ替えたりできます。