Common Lispの利点

Lispは多くの場合、他の言語よりも有利な言語として宣伝されています。これは、独自の、統合された、便利な機能を備えているためです。



以下は、標準Common Lispの一連の機能を簡単に、そして例を挙げて強調する試みです。



この記事は、プログラミングの経験があり、Lispに興味があり、Lispが魅力的である理由をよりよく理解したい人に最も役立つと思われます。



テキストは、CL機能リストとRobert StrandhのCLレビューに基づいています。



豊富で正確な演算



Lispは、他の言語とうまく統合された豊富な数値型の階層を提供します。



長い数値 (bignum)は必要に応じて自動的に作成されるため、オーバーフローのリスクが軽減され、精度が確保されます。 たとえば、値10↑↑4をすばやく計算できます。



> (expt (expt (expt (expt 10 10) 10) 10) 10) 100000000000000000000000000000000000[...]
      
      





有理数は分数として表されるため、使用時に丸め誤差は発生しません。 正確な合理的な算術が言語に統合されています:



 > (+ 5/9 3/4) 47/36
      
      





複素数は、Lispの組み込みデータ型でもあります。 それらは短い構文として表すことができます:#c(10 5)は10 + 5iを意味します。 算術演算は、複雑な値でも機能します。



 > (* 2 (+ #c(10 5) 4)) #C(28 10)
      
      







一般化されたリンク



形状または場所は、個別の可変変数であるかのように使用できます。 SETFおよび他の同様の構造を使用して、特定の位置に概念的に関連付けられている値を変更できます。



たとえば、次のようにSETFを使用できます。



 > (defvar *colours* (list 'red 'green 'blue)) *COLOURS* > (setf (first *colours*) 'yellow) YELLOW > *colours* (YELLOW BLUE GREEN)
      
      





PUSHは次のようになります。



 > (push 'red (rest *colours*)) (RED BLUE GREEN) > *colours* (YELLOW RED BLUE GREEN)
      
      





汎用リンクは、リストに適用されるときだけでなく、他の多くの種類の構造やオブジェクトにも適用されます。 たとえば、オブジェクト指向プログラムでは、オブジェクトの一部のフィールドを変更する方法の1つはSETFを使用することです。



複数の値



リストなどの構造を明示的に作成しなくても、値を組み合わせることができます。 たとえば、(values 'foo' bar)は、2つの値-'foo and' barを返します。 このメカニズムを使用すると、関数は一度に複数の値を返すことができ、プログラムを簡素化できます。



たとえば、FLOORは2つの値を返す標準関数です。



 > (floor pi) 3 0.14159265358979312d0
      
      





慣例により、複数の値を返す関数は、デフォルトでは1つの値のみが返されるかのように使用されます-最初の値。

 > (+ (floor pi) 2) 5
      
      





この場合、残りの値を明示的に取得および使用できます。 次の例では、丸め時にPIの整数部分と小数部分を分離します。



 > (multiple-value-bind (integral fractional) (floor pi) (+ integral fractional)) 3.141592653589793d0
      
      







マクロ



Lispのマクロは、Lispフォームまたはオブジェクトを引数として取り、原則としてコードを生成し、コンパイルして実行する一種の関数です。 これは、マクロ展開と呼ばれる段階で、プログラムが実行される前に発生します。 マクロは、開発中に言語のすべての機能を使用して、ある種の計算を実行できます。



マクロの使用法の1つは、一部のソースコードを、既存の定義に関して正しいビューに変換することです。 つまり、マクロを使用すると、言語に新しい構文を追加できます(このアプローチは構文抽象化と呼ばれます)。



これにより、プログラムを実行する前に特別な構文を言語に追加できるため、Lispにドメイン固有言語(DSL)を簡単に埋め込むことができます。



マクロを使用する主な利点は、マクロが言語の機能を拡張し、プログラマーがより簡単に、より少ないコードでアイデアを表現できることです。 新しいツールを組み込みのように言語に追加できます。 さらに、マクロを使用してデータの事前計算または初期化を行うと、パフォーマンスの最適化に役立ちます。



マクロループ



LOOPマクロは、ループを表すための強力なツールです。 実際、これは反復プロセスを記述するためのまったく小さな組み込み言語です。 LOOPは、単純な繰り返しから反復子や複雑な状態マシンまで、ループを記述するために必要なすべてのタイプの式を提供します。



 > (defvar *list* (loop :for x := (random 1000) :repeat 5 :collect x)) *LIST* > *list* (324 794 102 579 55)
      
      





 > (loop :for elt :in *list* :when (oddp elt) :maximizing elt) 579
      
      





 > (loop :for elt :in *list* :collect (log elt) :into logs :finally (return (loop :for l :in logs :if (> l 5.0) :collect l :into ms :else :collect l :into ns :finally (return (values ms ns))))) (5.7807436 6.6770835 6.3613024) (4.624973 4.0073333)
      
      







FORMAT関数



FORMAT関数は、データのフォーマット方法を記述する埋め込み言語をサポートします。 単純なテキストの置換に加えて、FORMAT命令は、条件、ループ、境界ケースの処理など、テキストを生成するためのさまざまなルールをコンパクトな形式で表現できます。



この関数を使用して名前のリストをフォーマットできます。



 (defun format-names (list) (format nil "~{~:(~a~)~#[.~; and ~:;, ~]~}" list))
      
      





 > (format-names '(doc grumpy happy sleepy bashful sneezy dopey)) "Doc, Grumpy, Happy, Sleepy, Bashful, Sneezy and Dopey." > (format-names '(fry laurie)) "Fry and Laurie." > (format-names '(bluebeard)) "Bluebeard."
      
      





FORMATは、画面への標準出力、文字列、またはその他のストリームにかかわらず、結果を指定されたストリームに転送します。



高階関数



Lispの関数は本当のファーストクラスのエンティティです。 機能オブジェクトは、動的に作成するか、パラメーターとして渡すか、結果として返すことができます。 したがって、 高階関数、つまり引数と戻り値自体が関数になり得る関数がサポートされています。



ここでは、引数がリストと別の関数(この場合は# '<)であるSORT関数の呼び出しを確認できます。



 > (sort (list 4 2 3 1) #'<) (1 2 3 4)
      
      





渡される関数の名前の代わりに、ラムダ式とも呼ばれる匿名関数を使用できます。 これらは、不要な名前でプログラムを詰まらせることなく、1回限りの使用のための関数を作成する場合に特に役立ちます。 一般に、それらは字句クロージャを作成するために使用できます。



この例では、MAPCARの最初の引数として使用する匿名関数を作成します。



 > (mapcar (lambda (x) (+ x 10)) '(1 2 3 4 5)) (11 12 13 14 15)
      
      





関数を作成するとき、コンテキストをキャプチャするため、完全な語彙的クロージャーを使用できます。



 (let ((counter 10)) (defun add-counter (x) (prog1 (+ counter x) (incf counter))))
      
      





 > (mapcar #'add-counter '(1 1 1 1)) (11 12 13 14) > (add-counter 50) 64
      
      







リスト処理



リストはLispの基本的な組み込みデータ型であるため、リストを操作するための広範な機能セットがあります。 このような関数とマクロのおかげで、リストを使用して他のデータ構造のプロトタイプをすばやく作成できます。



たとえば、通常のリストを使用して次のように作業できます。



 > (defvar *nums* (list 0 1 2 3 4 5 6 7 8 9 10 11 12)) *NUMS* > (list (fourth *nums*) (nth 8 *nums*)) (3 8) > (list (last *nums*) (butlast *nums*)) ((12) (0 1 2 3 4 5 6 7 8 9 10 11)) > (remove-if-not #'evenp *nums*) (0 2 4 6 8 10 12)
      
      





そして-連想リストを使用して



 > (defvar *capital-cities* '((NZ . Wellington) (AU . Canberra) (CA . Ottawa))) *CAPITAL-CITIES* > (cdr (assoc 'CA *capital-cities*)) OTTAWA > (mapcar #'car *capital-cities*) (NZ AU CA)
      
      







ラムダリスト



ラムダリストは、関数、マクロ、バインディングフォーム、およびその他の構造のパラメーターを定義します。 Lambdaリストは、必須、オプション、名前付き、テール(残り)およびオプションのパラメーター、およびデフォルト値などを定義します。 これにより、非常に柔軟で表現力豊かなインターフェイスを定義できます。



オプションのパラメーターでは、呼び出し元が値を指定する必要はありません。 それらに対してデフォルト値を定義できます。それ以外の場合、呼び出されたコードは値が提供されているかどうかを確認し、状況に応じて動作できます。



次の関数は、オプションの区切り文字パラメーターを受け入れます。デフォルト値は空白文字です。



 (defun explode (string &optional (delimiter #\Space)) (let ((pos (position delimiter string))) (if (null pos) (list string) (cons (subseq string 0 pos) (explode (subseq string (1+ pos)) delimiter)))))
      
      





EXPLODE関数を呼び出すとき、オプションのパラメーターを指定するか、省略することができます。



 > (explode "foo, bar, baz" #\,) ("foo " " bar " " baz")
      
      





 > (explode "foo, bar, baz") ("foo," "bar," "baz")
      
      





名前付きパラメーターはオプションのパラメーターに似ていますが 、名前で定義されているため、任意の順序で渡すことができます。 名前を使用すると、コードの可読性が向上し、複数のパラメーターを使用して呼び出しを行う際の一種のドキュメントとして機能します。



たとえば、次の2つの関数呼び出しを比較します。



 // In C: xf86InitValuatorAxisStruct(device, 0, 0, -1, 1, 0, 1);
      
      





 ;; In Lisp: (xf86-init-valuator-axis-struct :dev device :ax-num 0 :min-val 0 :max-val -1 :min-res 0 :max-res 1 :resolution 1)
      
      







ファーストクラスのエンティティとしてのシンボル



シンボルは、名前で完全に定義される一意のオブジェクトです。 たとえば、「fooは名前が「FOO」のキャラクターです。 シンボルは、識別子またはいくつかの抽象的な名前として使用できます。 文字の比較は、一定時間にわたって行われます。



シンボルは、関数と同様に、一流のエンティティです。 それらは、動的に作成、引用(引用、未評価)、保存、引数として渡され、比較、文字列に変換、エクスポートおよびインポートでき、参照できます。



ここで、「* foo *」は変数の識別子です。



 > (defvar *foo* 5) *FOO* > (symbol-value '*foo*) 5
      
      







ファーストクラスエンティティとしてのパッケージ



名前空間の役割を果たすパッケージもファーストクラスのオブジェクトです。 プログラムの実行中に作成、保存、結果として返されるため、コンテキストを動的に切り替えたり、名前空間を動的に変換したりすることができます。



次の例では、INTERNを使用してパッケージに文字を含めます。



 > (intern "ARBITRARY" (make-package :foo :use '(:cl))) FOO::ARBITRARY NIL
      
      





Lispには、現在のパッケージを指す特別な変数* package *があります。 現在のパッケージがFOOである場合、次を実行できます。



 > (in-package :foo) #<PACKAGE "FOO"> > (package-name *package*) "FOO"
      
      







特別な変数



Lispは、字句コンテキストに加えて変数の動的コンテキストをサポートします。 動的変数はいくつかの場合に役立つ可能性があるため、そのサポートにより最大限の柔軟性が得られます。



たとえば、一部のコードの出力をファイルなどの非標準ストリームにリダイレクトし、特別な変数* standard-output *のダイナミックリンクを作成できます。



 (with-open-file (file-stream #p"somefile" :direction :output) (let ((*standard-output* file-stream)) (print "This prints to the file, not stdout.")) (print "And this prints to stdout, not the file."))
      
      





*標準出力*に加えて、lispには、*標準入力*、*パッケージ*、*読み取り可能*、*印刷可能*、*印刷円など、リソースとパラメーターを含むプログラムの状態を格納するいくつかの特殊変数が含まれます。など



コントロール転送



Lispには、制御を呼び出し階層の上位にあるポイントに移す2つの方法があります。 この場合、語彙領域または動的領域は、それぞれローカル遷移および非ローカル遷移に考慮することができます。



名前付きブロックにより、ネストされたフォームは、BLOCKおよびRETURN-FROMを使用して、名前付きの親フォームからコントロールを返すことができます。



たとえば、ここでネストされたループは、外側のループをバイパスして、初期ブロックからリストを返します。



 > (block early (loop :repeat 5 :do (loop :for x :from 1 :to 10 :collect x :into xs :finally (return-from early xs)))) (1 2 3 4 5 6 7 8 9 10)
      
      





キャッチ/スローは、非ローカルgotoのようなものです。 THROWは、最後に検出されたCATCHにジャンプし、パラメーターとして指定された値を渡します。



前の例に基づいたTHROW-RANGE関数では、プログラムの動的状態を使用して、THROWとCATCHを適用できます。



 (defun throw-range (ab) (loop :for x :from a :to b :collect x :into xs :finally (throw :early xs)))
      
      





 > (catch :early (loop :repeat 5 :do (throw-range 1 10))) (1 2 3 4 5 6 7 8 9 10)
      
      





動的な状態を考慮する必要がある場合に、レキシカルスコープを使用してキャッチ/スローするだけで十分な場合。



条件の再起動



Lispの条件システムは、プログラムの部分間で信号を送信するためのメカニズムです。



考えられる使用法の1つは、JavaまたはPythonで行うのとほぼ同じ方法で、例外をスローして処理することです。 しかし、他の言語とは異なり、Lispでのシグナル送信中にスタックは拡張しないため 、すべてのデータが保存され、シグナルハンドラーはスタック上の任意のポイントからプログラムを再起動できます。



例外を処理するこのアプローチにより、タスクの分離が改善され、より構造化されたコードを実現できます。 しかし、そのようなメカニズムは、プログラムの各部分間で(エラーだけでなく)任意のメッセージを送信するなど、より広い範囲を持っています。



条件システムの使用例は、記事Common Lisp:A Tutorial on Conditions and Restartsで見ることができます。



一般化された関数



Common Lispオブジェクトシステム(Common Lisp Object System、CLOS)は、メソッドをクラスにバインドしませんが、一般化された関数の使用を許可します。



汎用関数は、いくつかの異なるメソッドが満たすことができる署名を定義します。 呼び出されると、引数に最も一致するメソッドが選択されます。



ここで、キーボードからのイベントを処理する一般化された関数を定義します。

 (defgeneric key-input (key-name))
      
      





次に、異なるKEY-NAME値を満たすいくつかのメソッドを定義します。



 (defmethod key-input (key-name) ;; Default case (format nil "No keybinding for ~a" key-name)) (defmethod key-input ((key-name (eql :escape))) (format nil "Escape key pressed")) (defmethod key-input ((key-name (eql :space))) (format nil "Space key pressed"))
      
      





アクションのメソッド呼び出しを見てみましょう:



 > (key-input :space) "Space key pressed" > (key-input :return) "No keybinding for RETURN" > (defmethod key-input ((key-name (eql :return))) (format nil "Return key pressed")) > (key-input :return) "Return key pressed"
      
      





私たちは、コンストラクションなしで、スイッチとメソッドのテーブルを使用した明示的な作業を行いました。 したがって、新しい特別なケースの処理を、必要に応じて独立して、動的に、通常はプログラムのどこにでも追加できます。 これにより、特にボトムアップのLispプログラムの開発が保証されます。



一般化関数は、メソッドのグループのいくつかの共通の特性を定義します。 たとえば、メソッドの組み合わせのメソッド、特殊化オプション、およびその他のプロパティは、汎用関数によって指定できます。



Lispは多くの便利な標準汎用関数を提供します。 たとえば、PRINT-OBJECTは、テキスト表現を定義するために任意のクラスに特化できます。



メソッドの組み合わせ



メソッドの組み合わせによりメソッドを呼び出すときに、ある順序で、または一部の関数が他の関数の結果を処理するように、メソッドのチェーン全体を実行できます。



特定の順序でメソッドを配置するメソッドを結合するための組み込みメソッドがあります。 キーワードbefore 、: after、またはaroundで提供されるメソッドは、呼び出しチェーンの適切な場所に配置されます。



たとえば、前の例では、各KEY-INPUTメソッドは、「キーが押されました」というフレーズの出力を繰り返します。 次のような組み合わせでコードを改善できます:around



 (defmethod key-input :around (key-name) (format nil "~:(~a~) key pressed" (call-next-method key-name)))
      
      





その後、KEY-INPUTメソッドを再定義して、それぞれに1行のみを指定します。



 (defmethod key-input ((key-name (eql :escape))) "escape")
      
      





KEY-INPUTを呼び出すと、次のことが起こります。



デフォルトのオプションは異なる方法で処理できることに注意してください。 THROW / CATCHペアを使用するだけです(より高度な実装では条件を使用できます)。



 (defmethod key-input (key-name) (throw :default (format nil "No keybinding for ~a" key-name))) (defmethod key-input :around (key-name) (catch :default (format nil "~:(~a~) key pressed" (call-next-method key-name))))
      
      





その結果、統合されたメソッドの組み合わせメソッドにより、キーボードからのイベントの処理をモジュール化された拡張可能な簡単に変更可能なメカニズムに一般化することができます。 この手法は、ユーザー定義の組み合わせ方法を使用して補足できます。 たとえば、メソッドの結果を合計またはソートする組み合わせメソッドを追加できます。



多重継承



どのクラスにも多くの祖先を含めることができます。これにより、より豊富なモデルを作成し、より効率的なコードの再利用を実現できます。 子クラスの動作は、先祖クラスの定義に従って構築されたシーケンスに従って決定されます。



メソッドの組み合わせ、メタオブジェクトプロトコル、およびその他のCLOS機能を使用すると、従来の多重継承の問題(fork-joinなど)を回避できます。



メタオブジェクトプロトコル



メタオブジェクトプロトコル(MOP)は、CLOSを使用して実装されるCLOSプログラミングインターフェイスです。 MOPを使用すると、プログラマはCLOS自体を使用して内部CLOSデバイスを調査、使用、および変更できます。



ファーストクラスのエンティティとしてのクラス



クラス自体もオブジェクトです。 MOPを使用すると、クラスの定義と動作を変更できます。



FOOクラスをBARクラスの子孫とし、ENSURE-CLASS関数を使用して、たとえばBAZクラスをFOO先祖のリストに追加できます。



 (defclass bar () ()) (defclass foo (bar) ()) (defclass baz () ())
      
      





 > (class-direct-superclasses (find-class 'foo)) (#<STANDARD-CLASS BAR>) > (ensure-class 'foo :direct-superclasses '(bar baz)) #<STANDARD-CLASS FOO> > (class-direct-superclasses (find-class 'foo)) (#<STANDARD-CLASS BAR> #<STANDARD-CLASS BAZ>)
      
      





クラスの祖先に関する情報を取得するために、CLASS-DIRECT-SUPERCLASSES関数を使用しました。 この場合、FIND-CLASSから受け取ったオブジェクトの形式のクラスを引数として受け取ります。



上記の例は、プログラムの実行中にクラスを変更できるメカニズムを示しています。これにより、とりわけクラスにミックスインを動的に追加できます。



動的なオーバーライド



Lispは非常にインタラクティブでダイナミックな環境です。 関数、マクロ、クラス、パッケージ、パラメーター、オブジェクトはほぼいつでも再定義でき、結果は適切で予測可能です。



したがって、プログラムの実行中にクラスを再定義すると、変更はこのクラスのすべてのオブジェクトとサブクラスにすぐに適用されます。 radiusプロパティとそのサブクラスTENNIS-BALLでBALLクラスを定義できます。



 > (defclass ball () ((%radius :initform 10 :accessor radius))) #<STANDARD-CLASS BALL> > (defclass tennis-ball (ball) ()) #<STANDARD-CLASS TENNIS-BALL>
      
      





TENNIS-BALLクラスのオブジェクトは次のとおりです。radiusプロパティ用のスロットがあります。



 > (defvar *my-ball* (make-instance 'tennis-ball)) *MY-BALL* > (radius *my-ball*) 10
      
      





そして、別のボリュームスロットを追加することで、BALLクラスをオーバーライドできます。



 > (defclass ball () ((%radius :initform 10 :accessor radius) (%volume :initform (* 4/3 pi 1e3) :accessor volume))) #<STANDARD-CLASS BALL>
      
      





そして* MY-BALL *は自動的に更新され、先祖クラスで定義された新しいスロットを取得します。



 > (volume *my-ball*) 4188.790204786391d0
      
      







実行時のコンパイラーへのアクセス



COMPILEおよびCOMPILE-FILE関数のおかげで、Lispコンパイラは実行可能プログラムから直接使用できます。したがって、プログラムの実行中に作成または変更された関数もコンパイルできます。



プログラムは段階的にコンパイルできるため、開発がインタラクティブで動的かつ高速になります。起動したプログラムは、徐々に変更、デバッグ、および成長できます。



コンパイルマクロ



コンパイルマクロは、関数またはマクロをコンパイルするための代替戦略を定義します。通常のマクロとは異なり、コンパイルマクロは言語の構文を拡張せず、コンパイル時にのみ適用できます。したがって、これらは主にコード自体とは別にコードを最適化する方法を決定するために使用されます。



型定義



Lispは動的に型付けされた言語ですが、ラピッドプロトタイピングには非常に便利ですが、プログラマは変数の型を明示的に指定できます。これは、他のディレクティブと同様に、コンパイラーが言語が静的に型指定されているかのようにコードを最適化できるようにします。



たとえば、次のようにEXPLODE関数でパラメータタイプを定義できます。



 (defun explode (string &optional (delimiter #\Space)) (declare (type character delimiter) (type string string)) ...)
      
      







プログラマブルパーサー



Lispパーサーを使用すると、入力データを簡単に解析できます。入力ストリームからテキストを取得し、通常はS式と呼ばれるLispオブジェクトを作成します。これにより、入力データの分析が大幅に簡素化されます。



パーサーは、READ、READ-CHAR、READ-LINE、READ-FROM-STRINGなどのいくつかの関数を通じて使用できます。入力ストリームには、ファイル、キーボード入力などを使用できますが、さらに、適切な関数を使用して文字列または文字シーケンスからデータを読み取ることができます。



READ-FROM-STRINGを使用した読み取りの最も簡単な例は、文字列「(400 500 600)」からオブジェクト(400 500 600)、つまりリストを作成します。



 > (read-from-string "(400 500 600)") (400 500 600) 13 > (type-of (read-from-string "t")) BOOLEAN
      
      





リーダーマクロを使用すると、特定の構文に特別なセマンティクスを定義できます。これは、Lispパーサーがプログラム可能なため可能です。マクロの読み取りは、言語の構文を拡張する別の方法です(マクロは一般的に構文糖を追加するために使用されます)。



いくつかの標準的な読み取りマクロ:



パーサーは、読み取りルールが定義されているオブジェクトを生成できます。特に、これらのルールは読み取りマクロを使用して設定できます。実際、問題のパーサーは対話型インタープリター(読み取り-評価-印刷ループ、REPL)にも使用されます。



これは、標準の読み取りマクロを使用して16進表記で数値を読み取る方法です。



 > (read-from-string "#xBB") 187
      
      







プログラム可能な印刷



Lispのテキスト出力システムは、構造、オブジェクト、または他のデータを異なる形式で印刷する機能を提供します。



PRINT-OBJECTは、引数としてオブジェクトとストリームを受け取る組み込みの一般化された関数です。対応するメソッドは、指定されたオブジェクトのテキスト表現をストリームに出力します。いずれにせよ、オブジェクトのテキスト表現が必要な場合は、FORMAT、PRINT、REPLなどのこの関数が使用されます。



JOURNEYクラスを検討してください。



 (defclass journey () ((%from :initarg :from :accessor from) (%to :initarg :to :accessor to) (%period :initarg :period :accessor period) (%mode :initarg :mode :accessor mode)))
      
      





クラスJOURNEYのオブジェクトを印刷しようとすると、次のようなものが表示されます。



 > (defvar *journey* (make-instance 'journey :from "Christchurch" :to "Dunedin" :period 20 :mode "bicycle")) *JOURNEY* > (format nil "~a" *journey*) "#<JOURNEY {10044DCCA1}>"
      
      





JOURNEYクラスのPRINT-OBJECTメソッドを定義し、それを使用してオブジェクトの何らかのテキスト表現を設定できます。



 (defmethod print-object ((j journey) (s stream)) (format s "~A to ~A (~A hours) by ~A." (from j) (to j) (period j) (mode j)))
      
      





オブジェクトは新しいテキストビューを使用します。



 > (format nil "~a" *journey*) "Christchurch to Dunedin (20 hours) by bicycle."
      
      






All Articles