Tutorial これは簡単なArc入門です。 あまりプログラミング経験が無い人やLisp経験がない読者のために書かれています。 それゆえにLisp入門でもあります。 Arcプログラムは式から成りたちます。 最も簡単な式は数と文字列のようなもので、それらはそれら自身として評価されます。 括弧に囲まれたいくつかの式もまた式です。 これらはリストと呼ばれます。 リストが評価されたとき、その要素は左から右へと評価され、残りの値は1番目の値(おそらくは関数)に渡されます。 式の値としては、それが返せるものなら何でも返します。 これが何が起こっているのか、です。 まず最初に、+、1、2が評価され、それぞれプラス関数、1、2を返します。 そして、1と2はプラス関数に渡され3を返し、3は式全体の値として返されます。 (マクロは、評価される前にリストを変更するので、ねじれが存在します。 マクロに付いては後で立ち入りましょう。) 式と評価はともに再帰的に定義できるので、プログラムは以下のように好きなだけ複雑にする事が出来ます: "1 + 2"と書くのに慣れているとしたら、+を数値の前に置くのは変に見えますが、+は2個だけではなく、好きな個数だけ引数を取れるのが利点です: 特にLispでは一般的ですが、コードを生成させるとき、これが便利な表記法である事が分かるでしょう。 ArcのようなLisp方言は、ほとんどの言語が持っていないシンボルと言うデータ型を持っています。 私たちは既に1つ学びました。 +はシンボルです。 シンボルは数や文字列のように自分自身として評価されません。 シンボルは割り当てられた値を返すのです。 値13をfooに与えれば、fooが評価されると13を返します: 単独の引用符を式の前に置くことによって、評価を止めることができます。 従って、'fooはシンボルfooを返します。 特に観察力が鋭い読者は、 =の第1引数としてfooが使われたのにどうしてうまくいってるのか不思議に思っているかもしれません。引数が左から右に評価されるのなら、なぜfooが評価されたときにエラーを引き起こさなかったのか? 普通の評価規則に反するいくつかのオペレータがあり、=はそのひとつです。 第1引数は評価されません。リストをクオートすれば、リスト自体が戻ってきます。 最初の式は数値3を返します。 2番目の式はクオートされているので、シンボル+と数値1と2で成るリストを返します。 consはリストを組み上げ、先頭に新しい要素を追加したリストを返します: これはオリジナルのリストを変更しません: 空リストはシンボル nilによって表され、それ自体として評価されるように定義されています。 要素1つのリストを作るなら:最初の要素を返すcarと最初の要素以外の全てを返すcdrでリストを分解できます。 連続するconsである、多くの要素を含むリストを作成するにはlistを使います: リストにはどんな型の要素も入れられるのに注目してください。 consの呼び出しの終わりには4つの括弧があります。 Lispプログラマはどのようにこれに対処してるのでしょうか? してません。 式の右括弧は増減出来ますが、大部分の人は気づかないでしょう。 Lispプログラマは括弧を数えません。 彼らは括弧ではなくインデントでコードを読み、コードを書くときにはエディタに括弧を任せます。 (viでは:set sm、EmacsではM-x lisp-mode を使用してください) Common Lispの割り当てのように、Arcの =は変数だけではなく、内部構造に達することができます。 つまり、リストを変更するのに=を使用できるのです:リストは非常にフレキシブルなので、探索型プログラミングに役立ちます。 進み具合に応じて、リストが表現することに必ずしも縛られる必要はありません。 例えば、飛行機の座標を表現するのに2つの数のリストを使用できます。 誰かは2つのフィールド、xとyで座標のオブジェクトを定義するのが、より適切であると言うでしょう。 しかし、n次元に対応するためにプログラムを拡張するとき、座標を表すのにリストを使用していれば、しなければならないことは失われた座標のためにデフォルトをゼロとして新しいコードを作ることであり、残りの平面コードは平気で動き続けるでしょう。 または、別の方向に拡張し、部分的に評価される座標を許容すると決めるなら、座標の成分として変数を表すシンボルを使用し始めることができ、そして再び、既存のコードすべては動き続ける事でしょう。 探索型プログラミングでは、早過ぎる規格化を避けるのは早過ぎる最適化を避けるのと同じくらい重要です。 リストで表せる事で最もおもしろいものはコードです。 consで造るリストはプログラムを構成するコードと同じものです。 これは、プログラムを書くプログラムが書けることを意味します。 この普遍的な方法がマクロと呼ばれるものです。 のちにそれを説明しましょう。 まずは、関数から。 私たちは既にいくつかの関数、つまり+、cons、car、cdrを学びました。 新しい関数は defで定義し、シンボルを関数の名前として取り、シンボルのリストがパラメータを表し、次にゼロかより多くの式のリストをボディーとします。 関数が呼ばれると、シンボルが対応する引数に一時的に設定された("束縛"された)ボディー上で、それらの式は順番に評価されます。呼び出しの値としては最後の式が返すものを返します。 ここに2つの数値を取って、その平均を返す関数があります: 関数のボディーは1つの式、 (/ (+ x y) 2)から成り立ってます。関数が1つの式から成り立つのは、純粋に関数的なコード(副作用が無いコード)では一般的です。 =のようにdefはすべての引数を評価するというわけではありません。 defは特殊な評価規則をもっているオペレータの一つです。def式の値として返された不思議なオブジェクトは何でしょう? それは、関数のカタチです。 ほとんどのLispのように、Arcでの関数は数値や文字列のようなデータ型です。二重引用符によって囲まれた一連の文字が文字列を表すリテラルであるように、関数を表すリテラルはパラメタとボディーがあとに続いたシンボルfnで構成されたリストです。 そこで以下のように、2つの数値の平均を返す関数を表現することができます。 他の言語と違い、命名された関数に関して意味的に特別なものは何もありません。 defが行うすべては基本的にはこれです:そしてもちろん、値が1であるシンボルを使えるような場所でリテラルな関数を使用できます。例えば、 この式には3つの要素、平均を返す関数である (fn (x y) (/ (+ x y) 2))、数値2と4があります。つまり、すべての3つの式を評価して、2番目と3番目の値を1番目の値に渡すと、平均を返す関数に2と4を渡す事になり、結果は3となります。 シンボルや文字列のようなデータ型で出来て関数ではできない事が一つだけあります: 読み書きのやり方で出力はできません。 理由は、関数がクロージャであるかもしれないということです。 クロージャを出力するのは、トリッキーな問題なのです。 Arcでは、関数がどこにあろうがデータ構造を利用できて、インデックスリストだろうが何がデータ構造に保存されていようが、データ構造は関数のように振る舞う事が出来るのです。 従って、文字列の第1要素を手に入れるなら: ちなみに、返り値はリテラル文字になります。 また、データ構造が関数的位置にある式は =の第1引数のように働きます。一時的変数を設定するために一般的に使用される2つのオペレータ、 letとwithがあります。1つ目は1つの変数のためのものです。複数の変数を束縛するには withを使います。今までは、評価結果を暗黙に印字する場合だけを扱ってきました。 評価の途中で印字する標準のやり方が prかprnです。 prやprnは、複数の引数を取り、順番にそれらを印字します。 また、prnは終端に改行を印字します。 ここに、引数が何であったかを知らせるaverageの改良版があります:標準の条件演算子は ifです。 =やdefのように、ifはすべての引数を評価するというわけではありません。 3つの引数を与えると1番目を評価し、それがtrueを返したら、2番目の値、そうでなければ3番目の値を返します:
trueを返すとは、nil以外の全てを返す事を意味します。 Nilは慣習上、空リストと共に偽を表すために使われます。シンボル t(nilのように、それ自体として評価されます)は、真を表すのにしばしば使用されますが、nil以外のどんな値も表しています。偽と空リストに同じものを使用する事は時に混乱を生じますが、空リストは集合理論上は偽であり、多くのLispプログラムは集合で考えられるので、何年ものLispプログラミングが、それが掛け値なしの成功であると私に納得させました。 第3引数がない場合、 nilがデフォルトになります。引数が3つ以上の ifはネストされたifと等価です。は次に相当します。
elseifがある言語に慣れているなら、このパターンは身近でしょう。[1]
ifの各引数はただ一つの式なので、テスト結果に依存する複数のことをしたいのなら、doを使って一つの式として組み合わせます。何らかの条件が trueである時にいくつかの式を評価して欲しいなら、次のように書けます。しかしこの状況は非常に一般的なので、そのための別のオペレータがあります。
andとorは必要以上の引数を評価しないので、条件節のようなオペレータです。
否定オペレータは noと呼ばれ、空リストとしてのnilのように働きます。 ここに、リストの長さを返す関数があります:
リストが nilなら、関数はすぐに0を返すでしょう。 さもなければ、それはリストのcdrの長さより1多く返します。
lenと呼ばれる関数が既にあるので、この関数をmylenと名付けました。 Arcの関数を再定義するのは大歓迎ですが、lenはリスト以外にも働くので、lenを再定義してしまうとlenに依存したコードを壊す可能性があります。標準比較オペレータは isで、引数が同じか文字列が同じ文字を含んでいればtrueを返します。
isが同じ要素を持つ2つのリストにはfalseを返していることに注目してください。
そのための別のオペレータ、iso(isomorphicから)があります。
何かが選択肢の1つであるかどうかテストしたいなら、 (or (is x y) (is x z)…)と出来ますが、この状況は専用のオペレータが必要になるほど一般的です。
caseオペレータはキーと式を交互に取って、キーに適合する式の値を返します。最後の式がデフォルトとなります。
Arcには、さまざまなイテレーションオペレータがあります。 数の範囲では forを使用します。
リストや文字列の要素でイテレートするには eachを用います。
各回の終わりに見られるnilはループ内のコードによって印字されません。 それらはイテレーション式の返り値です。 何らかの条件が trueである間イテレーションを続けるにはwhileを使います。
また、めったに使わないでしょうが、Cの forオペレータと同様にn回何かをする、シンプルでより一般的なループオペレータであるrepeatオペレータが存在します:
map関数は、関数とリストを取って、連続した要素に関数を適用した結果を返します。
map関数は好きな長さのシーケンスを取ることができ、最短のシーケンスが尽きるまで関数を適用し続けます:
Lispプログラムでは1引数の関数がたびたび使用されるので、Arcにはそのための特別な記法があります。 [... _ ...]は(fn (_) (... _ ...))の略語です。従って、最初のmapの例は次のように記述出来ます。
変数を取り除くのは、プログラムをより短くする特に良い方法です。 不要な変数はプログラムの長さに何かを加える以上に概念的な負荷を増加させます。 名前の間にコロンを置くことによって、関数を合成できます。 例えば (foo:bar x y)は(foo (bar x y))と等価です。合成関数は引数としても便利です。
また、チルダ(~)を名前の前に置く事によって関数を否定できます:
mapのようなシーケンスの連続した要素に関数を適用する多くの関数があります。 もっとも一般的に使用される関数はkeepで、何らかのテストを満たした要素を返します。
他には、 keepの正反対のrem、 関数が全ての要素に関してtrueであるならtrueを返すall、 関数がいくつかの要素に関してtrueであるならtrueを返すsome、関数がtrueを返す最初の要素の位置を返すpos、 そして全てのnilじゃない値のリストを返すtruesがあります。
関数ではない第一引数をこのような関数に与えると、等価性テスト関数のように扱われます: そして、それらは全て文字列をリストと同様に扱います。 さまざまなデータ構造を表すのにはリストを使えますが、効率的にキー/値の組を格納したいならArcにもハッシュテーブルがあります。 値で満たされたハッシュテーブルを作成したい場合、キー/値のペアのリストを取って対応するハッシュテーブルを返すlisttabを使えます。 また、引数を分類したりキーをクオートする必要がない、簡略化フォームもあります。 リストや文字列のように、関数があるところならどこでもハッシュテーブルを使用できます。 関数keysはハッシュテーブル内のキーを返し、valsは値を返します。 ハッシュテーブルに対しては、リストに対するmapのようなmaptableと呼ばれる関数がありますが、これは新しいハッシュを返すのではなく、オリジナルのものを返します。 Lispには、マッカーシーの1960年の論文に遡るキー/値のペアを表現するリストを用いる伝統があります。注: 関数のように、読み書き出来るような形式ではハッシュテーブルは出力出来ません。これは改良予定です。 これは、連想リスト、別名alistと呼びます。 私はかつて、alistはただのハックだと思っていましたが、ハッシュテーブルには処理できない事、並べ替え、再帰関数での増加による組み上げ、テールを共有する構造、古い値の保存、などの可能な操作が含まれます。 関数 alrefはalistのキーに対応する最初の値を返します:文字列を組み立てる2、3のオペレータがあります。 最も一般的なものは stringで、複数の引数を取り、それらを文字列に詰め込みます。
無視されるnilを除いて、あらゆる引数がprによって印字されるように現れます。 tostringはdoみたいですが、ボディー内の評価中に生成されるどんな出力も文字列に送り、それを全体の式の値として返します。
typeを使えばデータの型が分かり、coerceを使えばそれを新しい型に変換できます。
pushとpopオペレータはスタックとしてリストを扱い、pushは先頭に新しい要素をプッシュして、popは先頭をポップします。
=のように、変数だけに働くのではなく、構造に対して働きます。
インクリメントやディクリメントには ++と--を用います。
また、何かをそれに適用すると関数が返す結果に変更するzapと呼ばれるより一般的なオペレータがあります。 例えば、 (++ x) は (zap [+ _ 1] x) と等価です。sort関数は第1引数として与えられた関数に従ってソートされたシーケンスのコピーを返します。
sortはオリジナルを変更しないので、特定の変数の値を並べ替えしたい(ないしは構造を変更したい)のなら、zapを使用してください:
適切な場所に新しい要素を挿入することによって並べ替えたリストを作成したいのなら、insortを使ってください: 実際には、並べ替えをする必要があるものは数のリストだけではありません。 しばしば、値以外の何らかのプロパティに従って並べ替える必要があるでしょう。例えば、 Arcの sortは安全で、それは比較関数で同等と判断された要素の相対位置を変化させないことを意味します:
>や<以外の比較関数がたびたび必要になるので、Arcにはそれらを組み立てるcompare関数があります:
私たちは今までにオプショナル引数や異なった数の引数を取るいくつかの関数を学んできました。 パラメータをオプショナルにするには、 xの代わりに(o x)と書けば良いです。 オプショナルパラメータのデフォルトはnilとします。
関数は好きなだけ多くのオプショナルパラメータを持つことができますが、それらはパラメータ・リストの終わりに来ないとなりません。 オプショナルパラメータの名前の後に式を置くと、必要な時にデフォルト値を生成するために評価されます。 式は直前のパラメータを示しています。 好きなだけ引数を取る関数を作るには、最後のパラメータの前にピリオドとスペースを置き、そうすれば残りすべての引数の値はリストに束縛されます: 引数の残りを取るので、このタイプのパラメータは「レストパラメータ」と呼ばれます。 一つのパラメータですべての引数を関数へ集めたいのなら、全部のパラメータ・リストに代わりにパラメータを使ってください。 (この方法は思ったほど出鱈目ではないです。 パラメータ・リストが引数のフォームを反映してて、 nil以外の何かで終わっているリストは、例えば、(a b . c)で表されています。)引数のリストに関数を適用するには applyを用います:
レストパラメータと applyがあれば、可変長引数を取るaverageの別バージョンを書くことができます。
ここまでで、マクロを書き始めるのに十分な知識を得ました。 基本的にマクロは、コードを生成する関数です。 もちろん、コードを生成するのは簡単です。 listをただ呼び出すだけです。 マクロが提供するものは、このやり方でプログラム内にコードを生成する方法です。 ここで(かなりバカな)マクロ定義があります: マクロ定義はちょうど関数定義の defをmacに取り替えたように見えるのに注目してください。このマクロが意味してるのは、式( foo)がコード内に現れるときはいつも、関数呼び出しのような正常な方法でそれを評価するべきでないということです。 代わりに、そこをマクロ定義のボディー、(list '+ 1 2)を評価した結果で置き換えろ、と言うことです。これはマクロ呼び出しの「展開」と呼ばれます。 言い換えれば、上のように fooを定義すると、(foo)をコードのどこかに置くのは(+ 1 2)をそこへ置くのと同じ事となります。
これは一つも引数も取らないので、かなり役に立たないマクロです。 そこで、より役立つものを: 内臓オペレータである whenを再定義しちゃいました。 普通は危険なアイディアですが、幸い紹介した定義は既存のものと同じです。
上の定義では、第1要素が whenの式を評価するとき、以下を引数に適用した結果に置き換えます。
手作業で、どうなっているのか見てみましょう。 従って、Arcでは、 を評価する時、最初に、定義したマクロが と変化させて、評価時に上で見たような振舞いを生成します。 listとconsを使用すると式を組み立てるのが難しくなるので、ほとんどのLisp方言にはリスト生成がより簡単なbackquoteと呼ばれる省略記法があります。式の前に単独のオープンクオート文字(`)を置くと、普通のクオート(')のように評価を止めますが、 リストの中で式の前にコンマを置くと、その式だけは評価されます。 バッククオート式は穴があいたクオート式のようなものです。 また、バッククオート式の中のどこでもコンマアット(,@)を置くことができ、その場合、値(リストでしょうが)をいかなるリストの中にでも挿入します。 バッククオートを使うと、 whenの定義はより読みやすいものとなります。実際、これはArcのソースでの whenの定義です。マクロを理解する1つの鍵は、マクロ呼び出しが関数呼び出しでないことを覚えることです。 マクロ呼び出しは関数呼び出しに似ています。 マクロ定義は関数定義により似ています。 しかし、根本的に異なった事があります。 評価するのではなく、コードを変更しているのです。 マクロは参照の世界ではなく、名前の世界にいるのです。 例えば、この repeatの定義を見てください:
上手く動きそうでしょ? しかし、あるコンテクストで使用すると、奇妙なことが起こります。 展開形を調べると何が問題なのか一目瞭然です。 上のコードは次と等価です。 今度はバグは明白です。 マクロは繰り返し中でカウントを保持するのに変数xを使用しますが、それが印字しようとしているxの邪魔をしているのです。 この種のバグに関しておかしな心配をする人々もいます。 それで、Scheme委員会は「衛生的な」マクロのための恐らく誤ったプランを採用しました。 解決策は、初心者にマクロ呼び出しは関数呼び出しであるという幻想を抱かせないことのように思えます。 マクロを書く人たちは、マクロが名前の世界の住人であると言う事を覚えなければなりません。 値の世界で -- 例えば除数としてゼロを使用しない等 -- 間違った値を使用しないように、名前の世界では当然、間違った名前を使用しないように気をつけなければなりません。 repeatを直すには、xの代わりにソースコード内で使われないシンボルを使用します。 Arcでは、関数uniqを呼びます。 repeatの正しい定義(実際Arcソースのもの)は
マクロでの使用で、1つ以上 uniqが必要ならuniqに束縛したい変数のリストや変数を取るw/uniqを使用します。ここにdoの一種で、最後の引数の代わりに最初の引数の値を返すdo1の定義を紹介します(これは、何かをした後メッセージを印字したいんですが、その何かを返り値として、メッセージを返り値にしたくない場合役立ちます):
マクロ定義で時々、実際に変数を「捕らえたい」場合もあるでしょう。 whenの変形、変数itをテスト値に束縛するヴァージョンは非常に役立ちます:
ある意味、ユークリッドの原理を知っているならすべての定理を知っていると言うのと同じ意味に於いて、マクロに関して全て知る事が出来ました。 たくさんの事はこれら単純なアイディアから発しますが、それらが定義するテリトリーを探検するには何年もかかるでしょう。 少なくとも、私は何年もかかりました。 でも、これは続ける価値がある道のりです。 マクロ呼び出しは更なるマクロ呼び出しに拡張できるので、それにより膨大で複雑な式 -- 別の方法だと手書きしなければならないコード -- を生成することができるのです。 その上、マクロのレイヤで組み立てたプログラムは極めて保守しやすい事が分かるでしょう。 実際に観察する必要はないので、コードのいくつかの部分はコンパイラにたどり着く前に10や20段階のマクロ展開が起こるのは驚きに値しないし知る必要もありません。 |