Lispプログラマーは、Lispは非常に小さなプリミティブ操作のセットから構築できる強力な言語であることを自慢しています。と呼ばれる方言の通訳をゴルフで練習して、そのアイデアを実践しましょうtinylisp
。
言語仕様
この仕様では、結果が「未定義」と記述されている条件は、インタープリターで何でもできます:クラッシュ、サイレントフェール、ランダムgobbldegookの生成、または期待どおりの動作。Python 3のリファレンス実装はこちらから入手できます。
構文
tinylispでのトークンは(
、)
、または括弧やスペースを除く一個の以上の印刷可能なASCII文字の任意の文字列。(つまり、次の正規表現:[()]|[^() ]+
。)数字のみで構成されるトークンは、整数リテラルです。(先頭のゼロは大丈夫です。)非数字を含むすべてのトークンは、のようにも、数字に見える例、シンボルである123abc
、3.14
と-10
。トークンを区切る場合を除き、すべての空白(少なくとも、ASCII文字32および10を含む)は無視されます。
tinylispプログラムは一連の式で構成されています。各式は、整数、記号、またはs式(リスト)のいずれかです。リストは、括弧で囲まれた0個以上の式で構成されます。アイテム間にセパレータは使用されません。式の例を次に示します。
4
tinylisp!!
()
(c b a)
(q ((1 2)(3 4)))
整形式でない式(特に、括弧が一致しない式)は、未定義の動作をもたらします。(参照実装は、開いているかっこを自動的に閉じ、一致しない閉じたかっこで解析を停止します。)
データ型
tinylispのデータ型は、整数、記号、およびリストです。組み込み関数とマクロも型と見なすことができますが、出力形式は未定義です。リストには、任意のタイプの値をいくつでも含めることができ、任意の深さにネストできます。整数は、少なくとも-2 ^ 31〜2 ^ 31-1でサポートされる必要があります。
空のリスト(()
nilとも呼ばれる)と整数0
は、論理的に偽と見なされる唯一の値です。他のすべての整数、空でないリスト、ビルトイン、およびすべてのシンボルは論理的に真です。
評価
プログラム内の式は順番に評価され、それぞれの結果はstdoutに送信されます(出力のフォーマットについては後で説明します)。
- 整数リテラルはそれ自体に評価されます。
- 空のリスト
()
はそれ自体に評価されます。 - 1つ以上の項目のリストは、最初の項目を評価し、それを関数またはマクロとして扱い、残りの項目を引数として呼び出します。アイテムが関数/マクロでない場合、動作は未定義です。
- シンボルは名前として評価され、現在の関数でその名前にバインドされた値を提供します。名前が現在の関数で定義されていない場合、グローバルスコープでバインドされた値に評価されます。名前が現在のスコープまたはグローバルスコープで定義されていない場合、結果は未定義です(参照実装ではエラーメッセージが表示され、nilが返されます)。
組み込み関数とマクロ
tinylispには7つの組み込み関数があります。関数は、いくつかの操作を適用して結果を返す前に、各引数を評価します。
c
-cons [truct list]。値とリストの2つの引数を取り、リストの先頭に値を追加して取得した新しいリストを返します。h
-頭(車、Lisp用語では)。リストを取得し、リストの最初の項目を返します。nilが指定されている場合はnilを返します。t
-テール(Lisp用語ではcdr)。リストを取得し、最初のアイテム以外のすべてを含む新しいリストを返します。nilが指定されている場合はnilを返します。s
-引く。2つの整数を取り、最初のマイナス2番目を返します。l
- 未満。2つの整数を取ります。最初の値が2番目の値より小さい場合は1を返し、それ以外の場合は0を返します。e
-等しい。同じタイプの2つの値(整数、両方のリスト、または両方のシンボル)を取ります。2つが等しい(またはすべての要素で同一)場合は1を返し、そうでない場合は0を返します。組み込みの等価性のテストは未定義です(参照実装は期待どおりに機能します)。v
-評価。式を表す1つのリスト、整数、またはシンボルを取り、それを評価します。たとえば、行うこと(v (q (c a b)))
は行うことと同じ(c a b)
です。(v 1)
与える1
。
ここでの「値」には、特に指定がない限り、リスト、整数、シンボル、または組み込みが含まれます。関数が特定の型を取るようにリストされている場合、異なる型を渡すことは、間違った数の引数を渡すことと同様に未定義の動作です(参照実装は通常クラッシュします)。
tinylispには3つの組み込みマクロがあります。マクロは、関数とは異なり、操作を適用する前に引数を評価しません。
q
- 見積もり。1つの式を受け取り、評価せずに返します。たとえば、関数またはマクロとして(1 2 3)
呼び出そうとしますが、listを返すため、評価はエラーになります。評価すると、name にバインドされた値が得られますが、名前自体が得られます。1
(q (1 2 3))
(1 2 3)
a
a
(q a)
i
-場合。条件、iftrue式、iffalse式の3つの式を取ります。最初に条件を評価します。結果が偽(0
またはnil)の場合、iffalse式を評価して返します。それ以外の場合、iftrue式を評価して返します。返されない式は評価されないことに注意してください。d
-def。シンボルと式を取ります。式を評価し、グローバルスコープで名前として扱われる特定のシンボルにバインドし、そのシンボルを返します。名前を再定義しようとすると失敗します(黙って、メッセージが表示されるか、クラッシュします。参照実装はエラーメッセージを表示します)。注:を渡す前に名前を引用符で囲む必要はありd
ませんが、評価したくないリストまたはシンボルの場合は式を引用符で囲む必要があります(d x (q (1 2 3)))
。
間違った数の引数をマクロに渡すと、未定義の動作になります(参照実装がクラッシュします)。の最初の引数としてシンボルではないものを渡すことd
は、未定義の動作です(参照実装ではエラーは発生しませんが、その後値を参照することはできません)。
ユーザー定義の関数とマクロ
これらの10個のビルトインから開始して、新しい関数とマクロを作成することで言語を拡張できます。これらには専用のデータ型はありません。それらは特定の構造を持つ単なるリストです。
- 関数は、2つのアイテムのリストです。1つ目は、1つ以上のパラメーター名のリスト、または関数に渡される引数のリストを受け取る単一の名前のいずれかです(したがって、可変アリティ関数が許可されます)。2番目は、関数の本体である式です。
- マクロは関数と同じですが、パラメータ名の前にnilが含まれているため、3項目リストになっています。(nilで始まらない3項目リストの呼び出しは未定義の動作です。参照実装は最初の引数を無視し、マクロとしても扱います。)
たとえば、次の式は2つの整数を追加する関数です。
(q List must be quoted to prevent evaluation
(
(x y) Parameter names
(s x (s 0 y)) Expression (in infix, x - (0 - y))
)
)
そして、任意の数の引数を取り、最初の引数を評価して返すマクロ:
(q
(
()
args
(v (h args))
)
)
関数とマクロは、直接呼び出して、を使用して名前にバインドd
し、他の関数またはマクロに渡すことができます。
関数本体は定義時に実行されないため、再帰関数は簡単に定義できます。
(d len
(q (
(list)
(i list If list is nonempty
(s 1 (s 0 (len (t list)))) 1 - (0 - len(tail(list)))
0 else 0
)
))
)
ただし、上記は長さ関数を使用しないため、長さ関数を定義する良い方法ではないことに注意してください...
末尾呼び出し再帰
末尾呼び出し再帰は、Lispの重要な概念です。特定の種類の再帰をループとして実装し、呼び出しスタックを小さく保ちます。tinylispインタープリターは、適切な末尾呼び出し再帰を実装する必要があります!
- ユーザー定義関数またはマクロの戻り式が別のユーザー定義関数またはマクロの呼び出しである場合、インタープリターは再帰を使用してその呼び出しを評価してはなりません。代わりに、現在の関数と引数を新しい関数と引数で置き換え、呼び出しのチェーンが解決されるまでループする必要があります。
- ユーザー定義関数またはマクロの戻り式がの呼び出しである
i
場合、選択されたブランチをすぐに評価しないでください。代わりに、別のユーザー定義関数またはマクロの呼び出しかどうかを確認してください。その場合、上記のように関数と引数を交換します。これは、の任意の深くネストされたオカレンスに適用されi
ます。
末尾再帰は、直接再帰(関数がそれ自体を呼び出す)と間接再帰(関数a
がfunction b
を呼び出す[etc]を呼び出す関数を呼び出すa
)の両方で機能する必要があります。
末尾再帰の長さ関数(ヘルパー関数付きlen*
):
(d len*
(q (
(list accum)
(i list
(len*
(t list)
(s 1 (s 0 accum))
)
accum
)
))
)
(d len
(q (
(list)
(len* list 0)
))
)
この実装は、任意の大きなリストに対して機能し、最大整数サイズによってのみ制限されます。
範囲
関数パラメーターはローカル変数です(変更できないため、実際には定数です)。それらは、その関数のその呼び出しの本体が実行されている間はスコープ内にあり、より深い呼び出し中および関数が戻った後はスコープ外になります。グローバルに定義された名前を「シャドウ」することができ、それによりグローバル名を一時的に使用できなくします。たとえば、次のコードは41ではなく5を返します。
(d x 42)
(d f
(q (
(x)
(s x 1)
))
)
(f 6)
ただし、x
呼び出しレベル1では呼び出しレベル2からアクセスできないため、次のコードは41を返します。
(d x 42)
(d f
(q (
(x)
(g 15)
))
)
(d g
(q (
(y)
(s x 1)
))
)
(f 6)
常にスコープ内の名前は、1)現在実行中の関数のローカル名(存在する場合)、2)グローバル名のみです。
提出要件
入出力
インタープリターは、stdinまたはstdinまたはコマンドライン引数で指定されたファイルからプログラムを読み取ることができます。各式を評価した後、その式の結果を、後続の改行で標準出力に出力する必要があります。
- 整数は、実装言語の最も自然な表現で出力する必要があります。負の整数は、先頭にマイナス記号を付けて出力できます。
- シンボルは、引用符やエスケープを囲むことなく、文字列として出力する必要があります。
- リストは、すべての項目をスペースで区切って出力し、括弧で囲む必要があります。括弧内の空間は任意であり:
(1 2 3)
そして( 1 2 3 )
両方の可能な形式です。 - 組み込み関数とマクロの出力は未定義の動作です。(参照解釈では、それらがとして表示され
<built-in function>
ます。)
その他
参照インタープリターには、REPL環境と、tinylispモジュールを他のファイルからロードする機能が含まれています。これらは便宜上提供されており、このチャレンジには必要ありません。
テストケース
テストケースはいくつかのグループに分けられているため、より複雑なものに取り組む前に、より簡単なものをテストできます。ただし、それらをすべて1つのファイルにまとめてダンプした場合も正常に機能します。実行する前に、見出しと予想される出力を削除することを忘れないでください。
末尾呼び出し再帰を適切に実装すると、スタックオーバーフローを引き起こすことなく、最終的な(マルチパート)テストケースが返されます。リファレンス実装では、ラップトップで約6秒で計算されます。
-1
、を実行することで値-1を生成でき(s 0 1)
ます。
F
、関数G
ifのF
呼び出しG
(動的スコープの場合など)では使用できませんが、内部で定義されたネストされた関数のH
場合(lexicalスコープの場合)、関数では使用できません。テストケース5を参照してください。 「誤解を招く可能性があります。H
F