誰かが Lisp を私に売り込もうとしている。Lisp は、これまでにないすべてのことを実行できる超強力な言語であり、さらにいくつかのこともできる。
Lisp の力を示す実用的なコード例はありますか?
(できれば、通常の言語でコード化された同等のロジックと一緒に。)
誰かが Lisp を私に売り込もうとしている。Lisp は、これまでにないすべてのことを実行できる超強力な言語であり、さらにいくつかのこともできる。
Lisp の力を示す実用的なコード例はありますか?
(できれば、通常の言語でコード化された同等のロジックと一緒に。)
マクロが好きです。
これは、LDAP から人の属性を取り除くためのコードです。たまたまそのコードが横たわっていて、他の人にとって役立つと思いました。
一部の人々は、想定されるマクロのランタイム ペナルティについて混乱しているため、最後に説明を追加しました。
(defun ldap-users ()
(let ((people (make-hash-table :test 'equal)))
(ldap:dosearch (ent (ldap:search *ldap* "(&(telephonenumber=*) (cn=*))"))
(let ((mail (car (ldap:attr-value ent 'mail)))
(uid (car (ldap:attr-value ent 'uid)))
(name (car (ldap:attr-value ent 'cn)))
(phonenumber (car (ldap:attr-value ent 'telephonenumber))))
(setf (gethash uid people)
(list mail name phonenumber))))
people))
「let バインディング」は、LET フォームの外に消えるローカル変数と考えることができます。バインディングの形式に注意してください。それらは非常に似ており、LDAP エンティティの属性と、値をバインドする名前 (「ローカル変数」) のみが異なります。便利ですが、少し冗長で重複があります。
では、重複をすべてなくす必要がなかったらいいと思いませんか? 一般的なイディオムは WITH-... マクロで、値を取得できる式に基づいて値をバインドします。そのように機能する独自のマクロ WITH-LDAP-ATTRS を導入して、元のコードに置き換えてみましょう。
(defun ldap-users ()
(let ((people (make-hash-table :test 'equal))) ; equal so strings compare equal!
(ldap:dosearch (ent (ldap:search *ldap* "(&(telephonenumber=*) (cn=*))"))
(with-ldap-attrs (mail uid name phonenumber) ent
(setf (gethash uid people)
(list mail name phonenumber))))
people))
たくさんの線が突然消えて、たった 1 つの線に置き換えられたのを見ましたか? これを行う方法?もちろん、マクロを使用します -- コードを書くコードです! Lisp のマクロは、C/C++ でプリプロセッサを使用して見つけることができるものとはまったく異なる動物です。ここでは、 Lisp コードを生成する実際のLisp コード ( #define
cpp の毛羽立ちではない) を実行することができます。コードがコンパイルされます。マクロは、実際の Lisp コード、つまり通常の関数を使用できます。基本的に制限なし。
それでは、これがどのように行われたか見てみましょう。1 つの属性を置き換えるために、関数を定義します。
(defun ldap-attr (entity attr)
`(,attr (car (ldap:attr-value ,entity ',attr))))
逆引用符の構文はややこしいように見えますが、その内容は簡単です。LDAP-ATTRS を呼び出すと、(コンマ) の値を含むリストが吐き出されattr
、その後にcar
("リストの最初の要素" (実際にはコンスペア)) が続きます。実際には、呼び出される関数があります。first
も使用できます) によって返されたリストの最初の値を受け取りますldap:attr-value
。これは、コードをコンパイルするときに実行したいコードではないため (プログラムを実行するときに属性値を取得する必要があります)、呼び出しの前にコンマを追加しません。
ともかく。マクロの残りの部分に進みます。
(defmacro with-ldap-attrs (attrs ent &rest body)
`(let ,(loop for attr in attrs
collecting `,(ldap-attr ent attr))
,@body))
-syntax,@
は、実際のリストの代わりに、リストの内容をどこかに置くことです。
これで正しい結果が得られることを簡単に確認できます。マクロは、多くの場合、次のように記述されます。より単純にしたいコード (出力)、代わりに記述したいコード (入力) から開始し、入力が正しい出力を与えるまでマクロを成形し始めます。この関数macroexpand-1
は、マクロが正しいかどうかを教えてくれます。
(macroexpand-1 '(with-ldap-attrs (mail phonenumber) ent
(format t "~a with ~a" mail phonenumber)))
に評価されます
(let ((mail (car (trivial-ldap:attr-value ent 'mail)))
(phonenumber (car (trivial-ldap:attr-value ent 'phonenumber))))
(format t "~a with ~a" mail phonenumber))
展開されたマクロの LET バインディングを最初のコードと比較すると、同じ形式であることがわかります。
マクロは、コンパイル時に実行されるコードであり、通常の関数やマクロを好きなように呼び出すことができるというひねりが加えられています! これは、いくつかの引数を取り、いくつかの変換を適用してから、結果の s-exp をコンパイラに供給する、手の込んだフィルター以上のものではありません。
基本的に、言語の低レベルのプリミティブではなく、問題のドメインで見つかる動詞でコードを記述できます。ばかげた例として、次のことを考えてみましょう (when
まだ組み込みでない場合)::
(defmacro my-when (test &rest body)
`(if ,test
(progn ,@body)))
if
は組み込みのプリミティブで、ブランチで1 つのprogn
フォームのみを実行できます。複数のフォームが必要な場合は、 ::を使用する必要があります。
;; one form
(if (numberp 1)
(print "yay, a number"))
;; two forms
(if (numberp 1)
(progn
(assert-world-is-sane t)
(print "phew!"))))
新しい友人でmy-when
ある を使用すると、a) false ブランチがない場合はより適切な動詞を使用でき、b) 暗黙的なシーケンス演算子、つまりprogn
::を追加できます。
(my-when (numberp 1)
(assert-world-is-sane t)
(print "phew!"))
ただし、コンパイルされたコードに が含まれることはありませんmy-when
。これは、最初のパスですべてのマクロが展開されるため、実行時のペナルティが発生しないためです。
Lisp> (macroexpand-1 '(my-when (numberp 1)
(print "yay!")))
(if (numberp 1)
(progn (print "yay!")))
macroexpand-1
展開は 1 レベルのみであることに注意してください。拡張がさらに下に続く可能性があります (実際には、ほとんどの場合!)。ただし、最終的には、コンパイラ固有の実装の詳細に到達することになりますが、これは多くの場合あまり興味深いものではありません。しかし、結果を拡大し続けると、最終的には詳細が得られるか、入力 s-exp だけが返されます。
物事を明確にすることを願っています。マクロは強力なツールであり、私が気に入っている Lisp の機能の 1 つです。
私が思いつく最も良い例は、Paul Graham のOn Lispです。完全な PDF は、先ほど指定したリンクからダウンロードできます。また、 Practical Common Lispを試すこともできます (これも Web 上で完全に入手できます)。
実用的ではない例がたくさんあります。私はかつて、自分自身を解析し、そのソースを Lisp リストとして扱い、リストのツリー トラバーサルを実行し、waldo 識別子がソースに存在するか、または評価された場合に WALDO に評価される式を構築できる約 40 行の Lisp でプログラムを作成しました。 Waldo が存在しない場合は nil。返された式は、解析された元のソースに car/cdr への呼び出しを追加することによって作成されました。他の言語で 40 行のコードでこれを行う方法がわかりません。おそらく、perl はさらに少ない行数でそれを行うことができます。
この記事が役に立つかもしれません: http://www.defmacro.org/ramblings/lisp.html
とは言うものの、Lisp の能力を短く実用的な例で示すのは非常に難しいです。あなたのプロジェクトがある程度の大きさになると、Lisp の抽象化機能に感謝し、それを使っていてよかったと思うでしょう。一方、適度に短いコード サンプルでは、Lisp の優れた点を十分に説明することはできません。なぜなら、他の言語の事前定義された省略形は、ドメイン固有の抽象化を管理する Lisp の柔軟性よりも、小さな例では魅力的に見えるからです。
Lispにはたくさんのキラー機能がありますが、マクロは私が特に気に入っている機能です。言語が定義するものと私が定義するものの間にもう障壁がないからです。たとえば、CommonLispにはwhile構文がありません。歩きながら頭に実装したことがあります。それは簡単でクリーンです:
(defmacro while (condition &body body)
`(if ,condition
(progn
,@body
(do nil ((not ,condition))
,@body))))
Etvoilà!CommonLisp言語を新しい基本構造で拡張しました。これで、次のことができます。
(let ((foo 5))
(while (not (zerop (decf foo)))
(format t "still not zero: ~a~%" foo)))
どちらが印刷されますか:
still not zero: 4
still not zero: 3
still not zero: 2
still not zero: 1
Lisp以外の言語でそれを行うことは、読者の練習問題として残されています...
Common Lisp Object System (CLOS) とマルチメソッドが好きです。
すべてではないにしても、ほとんどのオブジェクト指向プログラミング言語には、クラスとメソッドの基本的な概念があります。次のPythonのスニペットでは、クラス PeelingTool と vegetables (Visitor パターンに似たもの) を定義しています。
class PeelingTool:
"""I'm used to peel things. Mostly fruit, but anything peelable goes."""
def peel(self, veggie):
veggie.get_peeled(self)
class Veggie:
"""I'm a defenseless Veggie. I obey the get_peeled protocol
used by the PeelingTool"""
def get_peeled(self, tool):
pass
class FingerTool(PeelingTool):
...
class KnifeTool(PeelingTool):
...
class Banana(Veggie):
def get_peeled(self, tool):
if type(tool) == FingerTool:
self.hold_and_peel(tool)
elif type(tool) == KnifeTool:
self.cut_in_half(tool)
peel
メソッドを PeelingTool に入れ、Banana にそれを受け入れさせます。ただし、PeelingTool クラスに属している必要があるため、PeelingTool クラスのインスタンスがある場合にのみ使用できます。
Common Lisp Object System のバージョン:
(defclass peeling-tool () ())
(defclass knife-tool (peeling-tool) ())
(defclass finger-tool (peeling-tool) ())
(defclass veggie () ())
(defclass banana (veggie) ())
(defgeneric peel (veggie tool)
(:documentation "I peel veggies, or actually anything that wants to be peeled"))
;; It might be possible to peel any object using any tool,
;; but I have no idea how. Left as an exercise for the reader
(defmethod peel (veggie tool)
...)
;; Bananas are easy to peel with our fingers!
(defmethod peel ((veggie banana) (tool finger-tool))
(with-hands (left-hand right-hand) *me*
(hold-object left-hand banana)
(peel-with-fingers right-hand tool banana)))
;; Slightly different using a knife
(defmethod peel ((veggie banana) (tool knife-tool))
(with-hands (left-hand right-hand) *me*
(hold-object left-hand banana)
(cut-in-half tool banana)))
チューリング完全な言語であれば、何でも書くことができます。言語間の違いは、同等の結果を得るためにジャンプする必要があるフープの数です。
マクロや CLOS などの機能を備えたCommon Lispのような強力な言語を使用すると、標準以下のソリューションに落ち着いたり、カンガルーになったりするほど多くのフープをジャンプすることなく、すばやく簡単に結果を得ることができます。
実際、良い実用的な例は Lisp LOOP マクロです。
http://www.ai.sri.com/pkarp/loop.html
LOOP マクロは単純に Lisp マクロです。それでも、基本的にはミニ ループ DSL (ドメイン固有言語) を定義します。
この小さなチュートリアルを参照すると、(初心者であっても) コードのどの部分が Loop マクロの一部で、どれが「通常の」Lisp であるかを知るのが難しいことがわかります。
そして、それは Lisp の表現力の重要な構成要素の 1 つであり、新しいコードが実際にはシステムと区別できないということです。
たとえば、Java では、プログラムのどの部分が標準の Java ライブラリに由来するものなのか、独自のコードやサード パーティのライブラリに由来するものなのかを (一目で) 判断できない場合がありますが、コードのどの部分がどの部分であるかはわかっています。単なるクラスのメソッド呼び出しではなく、Java 言語です。確かに、それはすべて「Java 言語」ですが、プログラマーとして、アプリケーションをクラスとメソッド (そして現在は注釈) の組み合わせとして表現することだけに制限されています。一方、Lisp では、文字通りすべてが手に入ります。
Common Lisp を SQL に接続するための Common SQL インターフェースを考えてみましょう。http://clsql.b9.com/manual/loop-tuples.htmlでは、CL ループ マクロを拡張して SQL バインディングを「第一級市民」にする方法を示しています。
また、"[select [first-name] [last-name] :from [employee] :order-by [last-name]]" などの構成も確認できます。これは CL-SQL パッケージの一部であり、「リーダー マクロ」として実装されています。
Lisp では、マクロを作成してデータ構造や制御構造などの新しい構造を作成できるだけでなく、リーダー マクロを使用して言語の構文を変更することもできます。ここでは、リーダー マクロ (この場合は「[」記号) を使用して SQL モードにドロップインし、他の多くの言語のように生の文字列としてではなく、埋め込み SQL のように SQL を機能させています。
アプリケーション開発者としての私たちの仕事は、プロセスと構造をプロセッサが理解できる形式に変換することです。つまり、コンピューター言語は私たちを「理解していない」ため、必然的にコンピューター言語に「話しかける」必要があります。
Common Lisp は、アプリケーションをトップダウンで構築できるだけでなく、言語と環境を持ち上げて途中で満たすことができる数少ない環境の 1 つです。両端でコーディングできます。
これは可能な限りエレガントですが、万能薬ではありません. 明らかに、言語と環境の選択に影響を与える他の要因があります。しかし、それは確かに学び、遊ぶ価値があります。Lisp を学ぶことは、他の言語であっても、プログラミングを進歩させる素晴らしい方法だと思います。
この記事は非常に興味深いものでした。
この記事の著者である Brandon Corfman は、Java、C++、および Lisp でのソリューションをプログラミングの問題と比較した調査について書いており、C++ で独自のソリューションを記述しています。ベンチマーク ソリューションは、Peter Norvig の 45 行の Lisp (2 時間で作成) です。
Corfman は、彼のソリューションを C++/STL の 142 行未満に減らすのは難しいことに気付きました。その理由についての彼の分析は、興味深い読み物です。
Lisp(およびSmalltalk )システムについて私が最も気に入っているのは、それらが生きていると感じることです。Lispシステムの実行中に簡単にプローブおよび変更できます。
これが不思議に聞こえる場合は、Emacsを起動し、Lispコードを入力してください。タイプC-M-x
してボイラー!Emacs内からEmacsを変更しただけです。実行中にすべてのEmacs機能を再定義することができます。
もう1つのことは、コード=リストの同等性により、コードとデータの間のフロンティアが非常に薄くなることです。また、マクロのおかげで、言語を拡張して迅速なDSLを作成するのは非常に簡単です。
たとえば、コードが生成されたHTML出力に非常に近い基本的なHTMLビルダーをコーディングすることができます。
(html
(head
(title "The Title"))
(body
(h1 "The Headline" :class "headline")
(p "Some text here" :id "content")))
=>
<html>
<head>
<title>The title</title>
</head>
<body>
<h1 class="headline">The Headline</h1>
<p id="contents">Some text here</p>
</body>
</html>
Lispコードでは、自動インデントにより、終了タグがないことを除いて、コードが出力のように見えます。
http://common-lisp.net/cgi-bin/viewcvs.cgi/cl-selenium/?root=cl-seleniumのこのマクロの例が好きです。これ はSelenium(Webブラウザーテストフレームワーク)へのCommon Lispバインディングですが、すべてのメソッドをマッピングする代わりに、コンパイル時にSelenium独自のAPI定義XMLドキュメントを読み取り、マクロを使用してマッピングコードを生成します。生成されたAPIは次の場所で確認できます:common-lisp.net/project/cl-selenium/api/selenium-package/index.html
これは基本的に、外部データを使用してマクロを駆動します。この場合はXMLドキュメントですが、データベースまたはネットワークからの読み取りと同じくらい複雑である可能性があります。これは、コンパイル時にLisp環境全体を利用できるようにする力です。
私は1970年代にMITでAIの学生でした。他のすべての学生と同じように、私は言語が最も重要だと思いました。それにもかかわらず、Lispは主要言語でした。これらは私がまだそれがかなり良いと思ういくつかのことです:
記号数学。式の記号微分と代数的単純化を書くのは簡単で有益です。私はCでそれらを実行しますが、それでもそれらを実行します。
定理証明。挿入ソートが正しいことを証明しようとするように、時々私は一時的なAIビンジに行きます。そのために私は記号操作を行う必要があり、私は通常Lispに頼ります。
小さなドメイン固有言語。Lispは実際には実用的ではないことは知っていますが、解析などにすべてを巻き込むことなく、少しDSLを試してみたい場合は、Lispマクロを使用すると簡単にできます。
ミニマックスゲームツリー検索のような小さな遊びのアルゴリズムは、3行のように実行できます。
主にLispが私のために行うことは精神的な運動です。そうすれば、それをより実用的な言語に引き継ぐことができます。
PSラムダ計算と言えば、1970年代に同じAIミリユーで始まったのは、OOがすべての人の脳に侵入し始めたことであり、どういうわけか、それが何に役立つかについての関心が非常に混雑しているようです。つまり、機械学習、自然言語、視覚、問題解決に取り組み、クラス、メッセージ、タイプ、ポリモーフィズムなどが前に出て、あらゆる種類が部屋の後ろに行きました。
私が気に入っていることの 1 つは、アプリケーションの状態を失うことなく、コードの「実行時」をアップグレードできることです。ある場合にのみ役立つものですが、有用な場合は、既に存在している (または、開発中の最小限のコストで) ゼロから実装するよりもはるかに安価です。特に、これには「ゼロからほとんどゼロ」のコストがかかるためです。
XML テンプレートを使用して Common Lisp を拡張する方法を参照してください: cl-quasi-quote XML の例,プロジェクト ページ,
(babel:octets-to-string
(with-output-to-sequence (*html-stream*)
<div (constantAttribute 42
someJavaScript `js-inline(print (+ 40 2))
runtimeAttribute ,(concatenate 'string "&foo" "&bar"))
<someRandomElement
<someOther>>>))
=>
"<div constantAttribute=\"42\"
someJavaScript=\"javascript: print((40 + 2))\"
runtimeAttribute=\"&foo&bar\">
<someRandomElement>
<someOther/>
</someRandomElement>
</div>"
これは基本的に Lisp のバックティック リーダー (リストの準クォート用) と同じですが、XML (特別な <> 構文にインストールされている)、JavaScript (`js-inline にインストールされている) などの他のさまざまなものにも機能します。 .
明確にするために、これはユーザーライブラリに実装されています! そして、静的な XML、JavaScript などのパーツを、ネットワーク ストリームに書き込む準備ができているUTF-8でエンコードされたリテラル バイト配列にコンパイルします。単純な,
(コンマ) を使用すると、Lisp に戻り、ランタイム生成データをリテラル バイト配列にインターリーブできます。
これは気弱な人向けではありませんが、ライブラリが上記をコンパイルすると、次のようになります。
(progn
(write-sequence
#(60 100 105 118 32 99 111 110 115 116 97 110 116 65 116 116 114 105 98
117 116 101 61 34 52 50 34 32 115 111 109 101 74 97 118 97 83 99 114
105 112 116 61 34 106 97 118 97 115 99 114 105 112 116 58 32 112 114
105 110 116 40 40 52 48 32 43 32 50 41 41 34 32 114 117 110 116 105
109 101 65 116 116 114 105 98 117 116 101 61 34)
*html-stream*)
(write-quasi-quoted-binary
(let ((*transformation*
#<quasi-quoted-string-to-quasi-quoted-binary {1006321441}>))
(transform-quasi-quoted-string-to-quasi-quoted-binary
(let ((*transformation*
#<quasi-quoted-xml-to-quasi-quoted-string {1006326E51}>))
(locally
(declare (sb-ext:muffle-conditions sb-ext:compiler-note))
(let ((it (concatenate 'string "runtime calculated: " "&foo" "&bar")))
(if it
(transform-quasi-quoted-xml-to-quasi-quoted-string/attribute-value it)
nil))))))
*html-stream*)
(write-sequence
#(34 62 10 32 32 60 115 111 109 101 82 97 110 100 111 109 69 108 101 109
101 110 116 62 10 32 32 32 32 60 115 111 109 101 79 116 104 101 114 47
62 10 32 32 60 47 115 111 109 101 82 97 110 100 111 109 69 108 101 109
101 110 116 62 10 60 47 100 105 118 62 10)
*html-stream*)
+void+)
参考までに、上記の 2 つの大きなバイト ベクトルは、文字列に変換すると次のようになります。
"<div constantAttribute=\"42\"
someJavaScript=\"javascript: print((40 + 2))\"
runtimeAttribute=\""
そして2番目のもの:
"\">
<someRandomElement>
<someOther/>
</someRandomElement>
</div>"
また、マクロや関数などの他の Lisp 構造とうまく結合します。さて、これをJSPと比較してください...
マクロが強力で柔軟な理由について、この説明をご覧になりましたか? 他の言語の例はありませんが、申し訳ありませんが、マクロであなたを売るかもしれません.
@マーク、
あなたの言っていることにはいくらかの真実がありますが、私はそれが必ずしも単純であるとは限りません.
プログラマーや一般の人々は、常にすべての可能性を評価して言語を切り替えることに時間を割くわけではありません。多くの場合、最初の言語を決定するのはマネージャー、または最初の言語を教える学校です...そしてプログラマーは、この言語がその言語よりも多くの時間を節約すると判断できる場合、特定のレベルに到達するのに十分な時間を投資する必要はありません.
さらに、Microsoft や Sun などの巨大な営利団体の支援を受けている言語は、そのような支援を受けていない言語と比較して、常に市場で有利であることを認めなければなりません。
元の質問に答えるために、Paul Graham はここで例を挙げようとしていますが、必ずしも私が望むほど実用的ではないことは認めています:-)
特に印象に残ったことの 1 つは、付属の CLOS が気に入らなければ、独自のオブジェクト指向プログラミング拡張機能を作成できることです。
それらの 1 つはGarnetにあり、もう 1 つは Paul Graham のOn Lispにあります。
非決定論的プログラミングを可能にするScreamerというパッケージもあります(これはまだ評価していません)。
さまざまなプログラミング パラダイムをサポートするように変更できる言語は、柔軟でなければなりません。
Eric Normand によるこの投稿が役に立つかもしれません。彼は、コードベースが成長するにつれて、アプリケーションに合わせて言語を構築できるようにすることで、Lisp がどのように役立つかを説明しています。これには多くの場合、早い段階で余分な労力がかかりますが、後で大きな利点が得られます.
マルチパラダイム言語であるという単純な事実が、言語を非常に柔軟にしています。
John Ousterhout は、1994 年に Lisp に関して次の興味深い観察を行いました。
言語設計者は、なぜこの言語またはその言語 がアプリオリに優れているか、または劣っている必要があるかについて議論するのが大好きですが、これらの議論のどれも実際にはそれほど重要ではありません. 最終的には、ユーザーが自分の足で投票すると、すべての言語の問題が解決されます。
[ある言語] が人々の生産性を高めるなら、彼らはそれを使用します。他のより優れた言語が登場すると (または既に存在する場合)、人々はその言語に切り替えるでしょう。これが律法であり、良いことです。法律は、Scheme (または他の Lisp 方言) はおそらく「正しい」言語ではない、と私に言っています。過去 30 年間、あまりにも多くの人が自分の足で投票してきました。