Scheme はLispファミリーのプログラミング言語の方言です。Scheme は 1970 年代にMIT コンピュータ科学および人工知能研究所(MIT CSAIL) で作成され、開発者のGuy L. SteeleとGerald Jay Sussmanによって、現在ラムダ ペーパーとして知られる一連のメモを通じて公開されました。Scheme は、レキシカル スコープを選択した最初の Lisp の方言であり、末尾呼び出し最適化を実行することを実装に要求した最初の方言でもあり、関数型プログラミングや再帰アルゴリズムなどの関連技術をより強力にサポートしました。また、第一級継続をサポートした最初のプログラミング言語の 1 つでもありました。Common Lispの開発につながる取り組みに大きな影響を与えました。[ 2 ]
Scheme 言語は、公式の電気電子学会(IEEE) 標準[ 3 ]と、アルゴリズム言語 Scheme に関するRevised n Report (R n RS)と呼ばれる事実上の標準で標準化されています。広く実装されている標準は R5RS (1998 年) です。[ 4 ] Scheme の最も最近承認された標準は「R7RS-small」(2013 年) です。[ 5 ]より拡張的でモジュール化された R6RS は 2007 年に承認されました。[ 6 ]どちらも R5RS から派生しており、以下のタイムラインは承認の時系列順を示しています。
Scheme は 1970 年代にCarl HewittのActor モデルを理解しようとする試みとして始まり、その目的のために Steele と Sussman はMaclispを使用して「小さな Lisp インタープリタ」を作成し、その後「アクターの作成とメッセージの送信のためのメカニズムを追加」しました。[ 7 ] Scheme は当初、 PlannerやConniverなどの他のLisp派生言語 の伝統に倣って「Schemer」と呼ばれていました。現在の名称は、ファイル名をそれぞれ最大 6 文字の 2 つのコンポーネントに制限するITS オペレーティングシステムを著者が使用していたことに由来します。現在、「Schemer」は Scheme プログラマを指すのに一般的に使用されています。
2003年のSchemeワークショップで、新たな言語標準化プロセスが開始され、2006年にR6RS標準を策定することを目標とした。このプロセスは、それまでのR n RSにおける全会一致のアプローチとは一線を画すものであった。
R6RSは標準モジュールシステムを採用しており、コア言語とライブラリを分離できる。R6RS仕様のドラフトがいくつか公開され、最終バージョンはR5.97RSとなった。投票の結果、新標準が承認され、2007年8月28日に発表された。[ 6 ]
現在、さまざまな Scheme 実装の最新リリース[ 8 ]は R6RS 標準をサポートしています。R6RS 用に提案されている暗黙的にフェーズされたライブラリの移植可能な参照実装である psyntax があり、さまざまな古い Scheme 実装で適切にロードおよびブートストラップされます。[ 9 ]
R6RS の特徴の一つは、レコード型記述子 (RTD) です。RTD が作成されて使用されると、レコード型表現によってメモリレイアウトが表示されます。また、オブジェクトフィールドのビットマスクと可変 Scheme オブジェクトフィールドのビットマスクも計算され、RTD に保存されているフィールドリスト全体を走査することなく、ガベージコレクタがフィールドに対して何をすべきかを知るのに役立ちます。RTD を使用すると、ユーザーは基本 RTD を拡張して新しいレコードシステムを作成できます。[ 10 ]
R6RS では、言語に数多くの重要な変更が導入されています。[ 11 ] ソースコードはUnicodeで指定されるようになり、Unicode 文字の大部分が Scheme のシンボルや識別子に使用できるようになりました。また、字句規則にもいくつかの小さな変更があります。文字データも Unicode で指定されるようになりました。多くの標準手続きが新しい標準ライブラリに移動され、それ自体が標準の大きな拡張となり、以前は標準の一部ではなかった手続きや構文形式が含まれています。新しいモジュールシステムが導入され、例外処理システムが標準化されました。構文規則は、マクロ展開時に Scheme のすべてを使用できるようにする、より表現力豊かな構文抽象化機能 (syntax-case) に置き換えられました。準拠する実装では、Scheme の完全な数値タワーをサポートすることが求められ、数値のセマンティクスが拡張されました。これは主に、浮動小数点数値表現のIEEE 754標準のサポートの方向です。
R6RS 標準は、ミニマリストの哲学からの逸脱と見なされているため、論争を引き起こしています。[ 12 ] [ 13 ] 2009 年 8 月、標準化プロセスを監督する Scheme 運営委員会は、Scheme を 2 つの言語に分割することを推奨する意向を発表しました。1 つはプログラマ向けの大規模な最新のプログラミング言語、もう 1 つは教育者や一般の実装者から称賛されているミニマリズムを維持した大規模バージョンのサブセットである小規模バージョンです。[ 14 ]これらの 2 つの新しいバージョンの Scheme に取り組むために 2 つのワーキング グループが作成されました。Scheme レポート プロセスのサイトには、ワーキング グループの憲章、公開ディスカッション、および問題追跡システムへのリンクがあります。
R7RSの第9草案(簡略版)は2013年4月15日に公開された。[ 15 ]この草案を承認する投票は2013年5月20日に締め切られ、[ 16 ]最終報告書は2013年8月6日から公開されており、「この取り組みの『簡略版』の記述について説明しているため、R6RSの後継として単独で考えることはできない」としている。[ 5 ]
Schemeは主に関数型プログラミング言語です。Lispプログラミング言語ファミリーの他の言語と多くの特徴を共有しています。Schemeの非常にシンプルな構文は、 S式に基づいています。S式とは、括弧で囲まれたリストで、前置演算子の後に引数が続きます。したがって、Schemeプログラムはネストされたリストのシーケンスで構成されます。リストはSchemeの主要なデータ構造でもあり、ソースコードとデータ形式の間に密接な等価性(ホモイコニシティ)をもたらします。Schemeプログラムは、Schemeコードの断片を動的に簡単に作成および評価できます。
リストをデータ構造として利用する点は、すべてのLisp方言に共通しています。Schemeは、Lispの祖先から、リスト処理のプリミティブである、といった豊富な機能を継承しています。Schemeは、厳密かつ動的に型付けされた変数を使用し、第一級の手続きをサポートしています。そのため、手続きを変数の値として代入したり、手続きへの引数として渡したりすることができます。conscarcdr
このセクションでは、主にSchemeの革新的な機能、特に他のLispと区別する機能に焦点を当てます。特に断りのない限り、機能の説明はR5RS規格に基づいています。このセクションで示されている例では、「===> result」という表記は、直前の行の式を評価した結果を示すために使用されています。これはR5RSで使用されている表記法と同じです。
Scheme は非常にシンプルな言語であり、同等の表現力を持つ他の多くの言語よりも実装がはるかに容易です。[ 17 ]この容易さは、ラムダ計算 を使用して言語の構文の大部分をより基本的な形式から導出していることに起因します。たとえば、R5RS Scheme 標準で定義されている 23 個の s 式ベースの構文構造のうち、14 個は派生形式またはライブラリ形式に分類され、主にラムダを含むより基本的な形式を含むマクロとして記述できます。R5RS (§3.1) には次のように書かれています。「変数束縛構造の中で最も基本的なのはラムダ式です。なぜなら、他のすべての変数束縛構造はラムダ式で説明できるからです。」[ 4 ]
例:変数バインディングを実行するためにlet式として実装するマクロ。lambda
( define-syntax let ( syntax-rules () (( let (( var expr ) ... ) body ... ) (( lambda ( var ... ) body ... ) expr ... ))))したがって、let上記のように定義すると、Scheme 実装では " (let ((a 1)(b 2)) (+ b a))" が " ((lambda (a b) (+ b a)) 1 2)" に書き換えられ、実装のタスクはプロシージャのインスタンス化をコーディングすることだけになります。
1998年、サスマンとスティールは、Schemeのミニマリズムは意識的な設計目標ではなく、設計プロセスの意図せざる結果であったと述べている。「私たちは実際には複雑なものを作ろうとしていましたが、偶然にも、すべての目標を満たしつつ、意図していたよりもはるかにシンプルなものを設計していたことに気づきました。...私たちは、小さくてシンプルな形式体系であるラムダ計算が、強力で表現力豊かなプログラミング言語の中核となり得ることに気づいたのです。」[ 7 ]
ほとんどの現代プログラミング言語と同様に、またMaclispのような初期のLispとは異なり、Schemeは字句スコープを採用しています。つまり、プログラムユニット内のすべての変数束縛は、呼び出しのコンテキストを考慮せずに、プログラムユニットのテキストを読むだけで分析できます。これは、当時のコンパイラやインタプリタで字句スコープアルゴリズムを実装するために使用されていた原始的なテキスト置換メソッドに伴う処理コストのために、初期のLisp方言の特徴であった動的スコープとは対照的です。当時のLispでは、プロシージャ内の自由変数への参照が、呼び出しのコンテキストに応じて、プロシージャ外部の全く異なる束縛を参照することが十分に可能でした。
1970年代初頭には珍しいスコープモデルであったレキシカルスコープをLispの新バージョンに組み込むきっかけとなったのは、サスマンによるALGOLの研究であった。彼は、 ALGOLのようなレキシカルスコープのメカニズムが、カール・ヒューイットのアクターモデルをLispに実装するという当初の目標を実現するのに役立つだろうと示唆した。[ 7 ]
Lisp 方言に語彙スコープを導入する方法に関する重要な洞察は、Sussman と Steele による 1975 年の Lambda Paper、「Scheme: An Interpreter for Extended Lambda Calculus」[ 18 ]で広く知られるようになりました。この論文では、語彙閉包の概念(21 ページ) を採用していますが、これは 1970 年にJoel MosesがAI Memoで説明したもので、 Peter J. Landinに帰属するアイデアでした。[ 19 ]
アロンゾ・チャーチの数学的記法であるラムダ計算は、Lisp で手続きを導入するためのキーワードとして「lambda」を使用するきっかけとなり、 Lisp で高階関数を使用する関数型プログラミング技術の開発にも影響を与えました。しかし、初期の Lisp は自由変数の扱い方からラムダ計算の適切な表現ではありませんでした。[ 7 ]
形式ラムダシステムには公理と完全な計算規則があります。これは、数学的論理とツールを使用した分析に役立ちます。このシステムでは、計算は方向性のある演繹として見ることができます。ラムダ計算の構文は、x、y、z、...、括弧、スペース、ピリオド、および記号λからの再帰式に従います。[ 20 ]ラムダ計算の機能には、次のものがあります。第一に、強力な数学的論理の出発点として機能します。第二に、機械の評価を模倣するために使用できるため、プログラマが実装の詳細を考慮する必要性を減らすことができます。最後に、ラムダ計算は実質的なメタ理論を作成しました。[ 21 ]
字句スコープの導入により、ラムダ記法のいくつかの形式と、実際のプログラミング言語でのそれらの表現との間に等価性を持たせることで問題が解決されました。SussmanとSteeleは、ラムダ式を単純な手続きのインスタンス化としてではなく、「制御構造と環境修飾子」として使用することで、ALGOLやFortranを含む他のプログラミング言語のすべての命令的および宣言的意味論、および他のLispの動的スコープをエレガントに導出するために新しい言語を使用できることを示しました。[ 22 ]彼らは、ラムダ論文の最初の論文でSchemeの最初の記述とともに継続渡しスタイルを導入し、その後の論文で、ラムダ計算のこの実用的な使用の生の力を実証しました。
Scheme は、特にALGOLなどの以前のブロック構造言語からブロック構造を継承しています。Scheme では、ブロックは、、および の3 つの束縛構造によって実装されます。たとえば、次の構造は、というシンボルが数値 10 に束縛されるブロックを作成します。letlet*letrecvar
( define var "goose" ) ;; ここでの var への参照はすべて "goose" にバインドされます( let (( var 10 )) ;; ステートメントはここに記述します。ここでの var への参照はすべて 10 にバインドされます。) ;; ここでの var への参照はすべて "goose" にバインドされますブロックはネストすることで、プログラマのニーズに応じて任意の複雑なブロック構造を作成できます。ブロック構造を使用してローカルバインディングを作成することで、そうでなければ発生する可能性のある名前空間の衝突のリスクを軽減できます。
、のバリアントの1つはlet、let*バインディングが同じコンストラクト内で以前に定義された変数を参照することを許可するもので、次のようになります。
( let* (( var1 10 ) ( var2 ( + var1 12 ))) ;; ただし、var1 の定義は var2 を参照できません)もう一方のバリアントは、相互に再帰的な手続きを互いに結びつけることがletrecできるように設計されています。
;; ホフスタッターの男性と女性の配列をペアのリストとして計算する( define ( hofstadter-male-female n ) ( letrec ( ( female ( lambda ( n ) ( if ( = n0 ) 1 ( -n ( male ( female ( -n1 ) )))))) ( male ( lambda ( n ) ( if ( = n0 ) 0 ( -n ( female ( male ( -n1 ) )))) ) )) ( let loop (( i0 ) ) ( if ( > i n ) ' ( ) ( cons ( cons ( female i ) ( male i ) ) ( loop ( + i1 ) ) )))) )(ホフスタッター-男性-女性8 )===> (( 1 . 0 ) ( 1 . 0 ) ( 2 . 1 ) ( 2 . 2 ) ( 3 . 2 ) ( 3 . 3 ) ( 4 . 4 ) ( 5 . 4 ) ( 5 . 5 ))(この例で使用されている定義については、ホフスタッターの男性と女性の分類を参照してください。)
単一のプロシージャに束縛されたすべてのプロシージャはletrec、名前によって互いに参照することができ、同じプロシージャ内で先に定義された変数の値も参照できますが、同じプロシージャ内で後に定義された値letrecを参照することはできません。letrec
`let` の派生形である「名前付き let」形式はlet、キーワードの後に識別子が付きますlet。これにより、`let` 変数は、指定された識別子を名前とするプロシージャの引数にバインドされ、プロシージャの本体は `let` 形式の本体となります。プロシージャを呼び出すことで、本体を必要に応じて繰り返すことができます。名前付き let は、反復処理を実装するために広く使用されています。
例:シンプルなカウンター
( let loop (( n 1 )) ( if ( > n 10 ) ' () ( cons n ( loop ( + n 1 )))))===> ( 1 2 3 4 5 6 7 8 9 10 )Scheme の他の手続きと同様に、名前付き let で作成された手続きは第一級オブジェクトです。
Scheme には反復構造 がありますdoが、 Scheme では反復を表現するために末尾再帰を使用する方が慣用的です。標準に準拠した Scheme 実装では、末尾呼び出しを最適化して、無制限の数のアクティブな末尾呼び出し (R5RS セクション 3.5) [ 4 ]をサポートすることが求められます。これはScheme レポートで適切な末尾再帰として説明されている特性です。これにより、Scheme プログラマーは、より直感的な場合もある再帰構造を使用して反復アルゴリズムを安全に記述できます。末尾再帰手続きと名前付き形式は、末尾再帰を使用した反復をサポートします。let
;; 0 から 9 までの正方形のリストを作成します: ;; 注: loop はラベルとして使用される任意の記号です。任意の記号で構いません。( define ( list- of -squares n ) ( let loop (( i n ) ( res ' ())) ( if ( < i 0 ) res ( loop ( -i 1 ) ( cons ( * i i ) res )))))(正方形のリスト9 ) ===> ( 0 1 4 9 16 25 36 49 64 81 )Scheme の継続は第一級オブジェクトです。Scheme は、プログラマが提供するプロシージャの仮引数に束縛されたエスケーププロシージャとして現在の継続をパックすることで、それをキャプチャするプロシージャcall-with-current-continuation( とも呼ばれる) を提供します。(R5RS セクション 6.4) [ 4 ]第一級継続により、プログラマはイテレータ、コルーチン、バックトラッキングなどの非ローカル制御構造を作成できます。call/cc
継続は、命令型プログラミング言語のreturn 文の動作をエミュレートするために使用できます。次の関数はfind-first、関数funcとリストが与えられた場合、が true を返すようなのlst最初の要素を返します。xlst(func x)
( define ( find-first func lst ) ( call-with-current-continuation ( lambda ( return-immediately ) ( for-each ( lambda ( x ) ( if ( func x ) ( return-immediately x ))) lst ) #f )))( find-first integer? ' ( 1/2 3/4 5.6 7 8/9 10 11 )) ===> 7 ( find-first zero? ' ( 1 2 3 4 )) ===> #f次の例は、プログラマーにとっておなじみの難問ですが、Schemeでは継続を第一級オブジェクトとして扱い、変数にバインドしたり、手続きへの引数として渡したりできることを示しています。
( let* (( yin (( lambda ( cc ) ( display "@" ) cc ) ( call-with-current-continuation ( lambda ( c ) c )))) ( yang (( lambda ( cc ) ( display "*" ) cc ) ( call-with-current-continuation ( lambda ( c ) c ))))) ( yin yang ))このコードを実行すると、カウントシーケンスが表示されます。@*@**@***@****@*****@******@*******@********...
Common Lispとは対照的に、Schemeではすべてのデータとプロシージャが共通の名前空間を共有しますが、Common Lispでは関数とデータが別々の名前空間を持つため、関数と変数が同じ名前を持つことが可能であり、関数を値として参照するには特別な表記法が必要です。これは、Schemeの統一された名前空間とCommon Lispの別々の名前空間を指して、「Lisp-1 vs. Lisp-2」の区別として知られています。[ 23 ]
Schemeでは、データの操作やバインドに使用されるプリミティブと同じものが、プロシージャのバインドにも使用できます。Common Lispの`and`defunや#'`and`のようなプリミティブはSchemeにはありません。
;; 数値にバインドされた変数: ( define f 10 ) f ===> 10 ;; ミューテーション (バインドされた値の変更) ( set! f ( + f f 6 )) f ===> 26 ;; 同じ変数にプロシージャを割り当てる: ( set! f ( lambda ( n ) ( + n 12 ))) ( f 6 ) ===> 18 ;; 同じ変数に式の結果を代入: ( set! f ( f 1 )) f ===> 13 ;; 関数型プログラミング: ( apply + ' ( 1 2 3 4 5 6 )) ===> 21 ( set! f ( lambda ( n ) ( + n 100 ))) ( map f ' ( 1 2 3 )) ===> ( 101 102 103 )この小節では、長年にわたって下されてきた設計上の決定事項について記述する。これらの決定事項は、この計画に特有の性格を与えてきたが、当初の設計から直接生じたものではない。
Scheme は、複素数型や有理数型を含む、比較的完全な数値データ型セットを規定しており、これは Scheme では数値タワーとして知られています (R5RS セクション 6.2 [ 4 ] )。標準ではこれらを抽象化として扱い、実装者に特定の内部表現を義務付けていません。
数値には正確さという性質がある。正確な数値は、他の正確な数値を含む一連の正確な演算によってのみ生成される。したがって、不正確さは伝染する。規格では、正確な数値を生成するすべての演算において、2つの実装が同等の結果を生成する必要があると規定している。
R5RS規格ではexact->inexact、inexact->exact数値の正確さを変更するために使用できる手順が規定されています。inexact->exactは「引数に数値的に最も近い正確な数値」を生成します。exact->inexactは「引数に数値的に最も近い不正確な数値」を生成します。 R6RS規格では、これらの手順はメインレポートから省略されていますが、標準ライブラリ(rnrs r5rs(6))でR5RS互換手順として規定されています。
R5RS 規格では、Scheme の実装は数値タワー全体を実装する必要はありませんが、「実装の目的と Scheme 言語の精神の両方に合致する一貫性のあるサブセット」を実装する必要があります (R5RS セクション 6.2.3)。[ 4 ] 新しい R6RS 規格では、タワー全体の実装と、「実質的に無制限のサイズと精度を持つ正確な整数オブジェクトと正確な有理数オブジェクト、および特定のプロシージャを実装して、正確な引数が与えられたときに常に正確な結果を返す」ことが求められています (R6RS セクション 3.4、セクション 11.7.1)。[ 6 ]
例1:正確な有理複素数をサポートする実装における正確な算術演算。
;; 3つの有理実数と2つの有理複素数の和( define x ( + 1/3 1/4 -1/5 -1/3i 405/50+2/3i )) x ===> 509/60+1/3i ;; 正確性をチェックします。( exact? x ) ===> #t例2:正確な有理数も複素数もサポートしないが、有理数表記の実数を受け入れる実装における同じ算術演算。
;; 4 つの有理実数の和( define xr ( + 1/3 1/4 -1/5 405/50 )) ;; 2 つの有理実数の和( define xi ( + -1/3 2/3 )) xr ===> 8.48333333333333 xi ===> 0.333333333333333 ;; 正確性をチェックします。( exact? xr ) ===> #f ( exact? xi ) ===> #fどちらの実装もR5RS規格に準拠していますが、2番目の実装は完全な数値タワーを実装していないため、R6RS規格には準拠していません。
delayこの制度は、様式と手続きを通じて評価の遅延をサポートしますforce。
( define a 10 ) ( define eval-aplus2 ( delay ( + a 2 ))) ( set! a 20 ) ( force eval-aplus2 ) ===> 22 ( define eval-aplus50 ( delay ( + a 50 ))) ( let (( a 8 )) ( force eval-aplus50 )) ===> 70 ( set! a 100 ) ( force eval-aplus2 ) ===> 22約束の元の定義の語彙的文脈は保持され、その値も最初の使用後も保持されますforce。約束は一度だけ評価されます。
これらのプリミティブは、プロミスと呼ばれる値を生成または処理し、ストリームなどの高度な遅延評価構造を実装するために使用できます。[ 24 ]
R6RS規格では、これらはもはやプリミティブではなく、R5RS互換性ライブラリ(rnrs r5rs(6))の一部として提供されます。
R5RSでは、delayおよびの実装案がforce示されており、プロミスを引数なしのプロシージャ(サンク)として実装し、メモ化を使用して、が呼び出される回数に関係なく、それが一度だけ評価されるようにしていますforce(R5RS セクション6.4)。[ 4 ]
SRFI 41 は、有限数列と無限数列の両方を非常に簡潔に表現することを可能にします。たとえば、これはSRFI 41 で定義された関数を使用したフィボナッチ数列の定義です。 [ 24 ]
;; フィボナッチ数列を定義します: ( define fibs ( stream-cons 0 ( stream-cons 1 ( stream-map + fibs ( stream-cdr fibs ))))) ;; 数列の 100 番目の数を計算します: ( stream-ref fibs 99 ) ===> 218922995834555169026ほとんどのLispでは、手続き引数の評価順序が指定されています。Schemeでは指定されていません。演算子位置の式が評価される順序を含め、評価順序は呼び出しごとに実装によって選択できます。唯一の制約は、「演算子式とオペランド式の同時評価の効果は、何らかの逐次評価順序と整合するように制約される」ことです。(R5RS セクション 4.1.3) [ 4 ]
( let (( ev ( lambda ( n ) ( display "評価中 " ) ( display ( if ( procedure? n ) "procedure" n )) ( newline ) n )) ) (( ev + ) ( ev 1 ) ( ev 2 ))) ===> 3評価1評価2評価手順ev は、渡された引数を記述し、その引数の値を返す手続きです。他の Lisp とは異なり、Scheme 式の演算子位置 (最初の項目) に式が現れることは、演算子位置にある式の結果が手続きである限り、完全に合法です。
1と2を加算するプロシージャ「+」を呼び出す際、式(ev +)、(ev 1)、(ev 2)は、並列評価されたかのような効果が生じない限り、任意の順序で評価できます。したがって、上記のサンプルコードを実行すると、標準のSchemeでは次の3行が任意の順序で表示されますが、1行のテキストが別の行のテキストと交互に表示されると、逐次評価の制約に違反するため、表示されません。
R5RS 標準およびそれ以降のレポートでは、マクロ システムを介して Scheme の構文を簡単に拡張できます。R5RS 標準では、プログラマが単純なパターン マッチングサブ言語を使用して言語に新しい構文構造を追加できる強力な衛生的マクロ システムが導入されました (R5RS セクション 4.3)。[ 4 ] これ以前は、衛生的マクロ システムは、R4RS 標準の付録に追いやられており、「低レベル」マクロ システムと並んで「高レベル」システムとして扱われ、どちらも Scheme の拡張機能として扱われ、言語の必須部分とはみなされていませんでした。[ 25 ]
衛生的なマクロシステム(マクロシステムとも呼ばれるsyntax-rules)の実装では、言語の他の部分の字句スコープを尊重する必要があります。これは、マクロ展開のための特別な命名規則とスコープ規則によって保証され、他のプログラミング言語のマクロシステムで発生する可能性のある一般的なプログラミングエラーを回避します。R6RS では、より洗練された変換システムであるマクロシステムが規定されておりsyntax-case、これは以前から R5RS Scheme の言語拡張として利用可能です。
;; 複数式の真分岐と偽分岐のない「if」のバリアントを実装するマクロを定義します。( define-syntax when ( syntax-rules () (( when pred exp exps ... ) ( if pred ( begin exp exps ... )))))マクロとプロシージャの呼び出しは、どちらもS式であるため、よく似ていますが、扱い方が異なります。コンパイラはプログラム内でS式に遭遇すると、まずそのシンボルが現在の字句スコープ内で構文キーワードとして定義されているかどうかを確認します。定義されている場合は、S式の末尾にある項目を評価コードをコンパイルせずに引数として扱い、マクロを展開しようとします。このプロセスは、マクロ呼び出しがなくなるまで再帰的に繰り返されます。構文キーワードでない場合は、コンパイラはS式の末尾にある引数を評価するコードをコンパイルし、次にS式の先頭にあるシンボルで表される変数を評価し、評価された末尾の式を引数として渡してプロシージャとして呼び出します。
ほとんどのScheme実装では、追加のマクロシステムも提供されています。よく使われるものとしては、構文クロージャ、明示的な名前変更マクロ、Common Lispで提供されているシステムdefine-macroに似た非衛生的なマクロシステムなどがあります。defmacro
マクロが衛生的かどうかを指定できないことは、マクロシステムの欠点の1つです。スコープセットなどの拡張のための代替モデルは、潜在的な解決策を提供します。[ 26 ]
R5RS 以前、Scheme にはeval他の Lisp で広く使われている手続きに相当する標準のものはなかったが、最初の Lambda Paper ではevaluate「LISP 関数 EVAL に似ている」と説明されており[ 18 ]、1978 年の最初の改訂レポートではenclose、2 つの引数を取る に置き換えられた。2 番目、3 番目、4 番目の改訂レポートでは、 に相当するものは省略されたeval。
この混乱の原因は、Scheme の字句スコープでは、式の評価結果が評価される場所によって異なるためです。たとえば、次の式の評価結果が 5 になるのか 6 になるのかは明確ではありません。[ 27 ]
( let (( name '+ )) ( let (( + * )) ( evaluate ( list name 2 3 ))))が定義されている外部環境で評価された場合name、結果はオペランドの合計になります。記号「+」がプロシージャ「*」の値にバインドされている内部環境で評価された場合、結果は2つのオペランドの積になります。
R5RS では、環境を返す 3 つの手続きを指定し、evals 式と環境を受け取り、指定された環境で式を評価する手続きを提供することで、この混乱を解消しています。(R5RS セクション 6.5) [ 4 ] R6RS では、プログラマが評価環境にインポートするオブジェクトを正確に指定できる手続きを提供することで、これを拡張していますenvironment。
evaluateこの式を評価するための最新の方式(通常はR5RSと互換性がある)では、次のような関数を定義する必要があります。
( define ( evaluate expr ) ( eval expr ( interaction-environment )))interaction-environment通訳者のグローバル環境である。
Common Lisp を含むほとんどの Lisp 方言では、慣例として、この値はNILブール式で false と評価されます。Scheme では、1991 年の IEEE 標準以降、[ 3 ]を除くすべての値(Scheme で と記述される の同等の値#fを含む) は、ブール式で true と評価されます。(R5RS セクション 6.3.1) [ 4 ]NIL'()
ほとんどのLispでは、真を表す定数は ですがT、Schemeでは です#t。
Scheme では、プリミティブデータ型は互いに排他的です。任意のScheme オブジェクトに対して真となる述語は、次の 1 つだけです。boolean?、pair?、symbol?、number?、char?、string?、、。 (R5RS sec 3.2) [ 4 ]vector?port?procedure?
一方、数値データ型では、数値は重複します。たとえば、整数値はinteger?、、、、および述語のすべてを同時に満たします。(R5RS sec 6.2)[ 4 ]rational?real?complex?number?
Schemeには、任意のオブジェクト間の3つの異なる等価性があり、それぞれ3つの異なる等価述語、等価性をテストするための関係演算子eq?、eqv?およびで表されますequal?。
eq?#fパラメータがメモリ内の同じデータオブジェクトを表していない限り、評価は になります。eqv?は一般的に と同じですが、eq?プリミティブ オブジェクト (文字や数値など) を特別に扱い、同じ値を表す数値がeqv?同じオブジェクトを参照していなくても となるようにします。equal?リスト、ベクトル、文字列などのデータ構造を比較して、構造とeqv?内容が一致しているかどうかを判定します。(R5RS セクション 6.1) [ 4 ]Schemeには型依存の等価性演算も存在します。string=?と はstring-ci=?2つの文字列を比較します(後者は大文字小文字を区別しない比較を実行します)。char=?と はchar-ci=?文字を比較します=。 は数値を比較します。[ 4 ]
R5RS 標準までは、Scheme の標準コメントはセミコロンで、これにより行の残りの部分が Scheme から見えなくなります。多数の実装では、コメントが複数行に及ぶことを許可する代替規約がサポートされており、R6RS 標準では 2 つの規約が許可されています。(SRFI 62 [ 28 ]で導入)を前に付けることで、 s 式全体をコメントに変換(または「コメントアウト」)できます。また、テキストを と で囲むことで、複数行のコメントまたは「ブロック コメント」を作成できます。#;#||#
Scheme の入力と出力はポートデータ型に基づいています。(R5RS sec 6.6) [ 4 ] R5RS は、標準入力と標準出力の Unix の概念に対応する、 プロシージャ と でアクセスできる 2 つのデフォルト ポートを定義していますcurrent-input-port。ほとんどの実装では も提供しています。入力と標準出力のリダイレクトは、やなどの標準プロシージャによって標準でサポートされています。ほとんどの実装では、同様のリダイレクト機能を備えた文字列ポートが提供されており、SRFI 6 で説明されているプロシージャを使用して、ファイルではなく文字列バッファに対して多くの通常の入出力操作を実行できます。[ 29 ] R6RS 標準では、より洗練された機能を持つポートプロシージャと多くの新しいタイプのポートが指定されています。current-output-portcurrent-error-portwith-input-from-filewith-output-to-file
以下の例は、厳密なR5RS方式で記述されています。
例1:出力がデフォルトで(現在の出力ポート)に設定されている場合:
( let (( hello0 ( lambda () ( display "Hello world" ) ( newline )))) ( hello0 ))例2:例1と同様だが、オプションのポート引数を使用してプロシージャを出力する。
( let (( hello1 ( lambda ( p ) ( display "Hello world" p ) ( newline p )))) ( hello1 ( current-output-port )))例3:例1と同様だが、出力先が新しく作成されたファイルにリダイレクトされる。
;; 注: with-output-to-file は R5RS のオプションの手順です( let (( hello0 ( lambda () ( display "Hello world" ) ( newline )))) ( with-output-to-file "helloworldoutputfile" hello0 ))例4:例2と同様だが、出力をファイルに送信するために明示的にファイルを開き、ポートを閉じる。
( let (( hello1 ( lambda ( p ) ( display "Hello world" p ) ( newline p ))) ( output-port ( open-output-file "helloworldoutputfile" ))) ( hello1 output-port ) ( close-output-port output-port ))例5:例2と同様だが、call-with-output-fileを使用して出力をファイルに送信する。
( let (( hello1 ( lambda ( p ) ( display "Hello world" p ) ( newline p )))) ( call-with-output-file "helloworldoutputfile" hello1 ))入力についても同様の手順が提供されています。R5RS Scheme は述語input-port?と を提供しますoutput-port?。文字の入出力には、write-char、read-char、peek-charがchar-ready?提供されています。Scheme 式の書き込みと読み取りには、Scheme はreadと を提供しますwrite。読み取り操作では、入力ポートがファイルの末尾に達した場合、返される結果はファイルの末尾オブジェクトであり、これは述語 を使用してテストできますeof-object?。
標準規格SRFI 28では、Common Lispのformat関数に似た基本的な書式設定手順も定義されており、その関数はCommon Lispの関数にちなんで名付けられている。[ 30 ]
Schemeでは、手続きは変数に束縛されます。R5RSでは、言語標準により、プログラムが組み込み手続きの変数束縛を変更し、実質的に再定義できることが正式に規定されました。(R5RS「言語の変更」)[ 4 ] 例えば、は、+再定義することで、数値だけでなく文字列も受け入れるように拡張できます。
( set! + ( let (( original+ + )) ( lambda args ( apply ( if ( or ( null? args ) ( string? ( car args ))) string-append original+ ) args )))) ( + 1 2 3 ) ===> 6 ( + "1" "2" "3" ) ===> "123"R6RSでは、標準バインディングを含むすべてのバインディングはライブラリに属し、エクスポートされたすべてのバインディングは不変です。(R6RS sec 7.1) [ 6 ] このため、標準プロシージャの変更による再定義は禁止されています。代わりに、標準プロシージャの名前で別のプロシージャをインポートすることは可能ですが、これは実質的に再定義に似ています。
標準Schemeでは、あるデータ型から別のデータ型に変換する手続きの名前には文字列「->」が含まれ、述語は「?」で終わり、既に割り当てられたデータの値を変更する手続きは「!」で終わります。これらの慣例は、Schemeプログラマーによってしばしば守られています。
Scheme 標準などの形式的な文脈では、ラムダ式またはプリミティブプロシージャを指す場合、「関数」よりも「プロシージャ」という用語が優先的に使用されます。通常の使用では、「プロシージャ」と「関数」は互換的に使用されます。プロシージャの適用は、形式的には「組み合わせ」と呼ばれることもあります。
他のLispと同様に、Schemeでは「 thunk 」という用語は引数のない手続きを指すのに使われます。「適切な末尾再帰」という用語は、すべてのScheme実装が末尾呼び出し最適化を実行して、無制限の数のアクティブな末尾呼び出しをサポートするという特性を指します。
R3RS「アルゴリズム言語スキームに関する改訂版レポート」以降の標準文書のタイトル形式は、 ALGOL 60標準文書「アルゴリズム言語Algol 60に関する改訂版レポート」のタイトルを参照しています。R3RSの要約ページは、ALGOL 60レポートの要約ページを忠実に模倣しています。[ 31 ] [ 32 ]
この言語は、R5RS (1998) [ 4 ]、R6RS (2007) [ 6 ]、およびR7RS (2013) [ 5 ]の規格で正式に定義されています。これらの規格では、言語の制御構造を提供するキーワードとそれに付随する構文、および一般的なタスクを実行する標準手順といった標準的な「形式」が記述されています。
この表は、Schemeにおける標準形式について説明しています。一部の形式は、言語内で単一の関数として容易に分類できないため、複数の行に記載されています。
この表で「L」とマークされた形式は、標準規格では派生「ライブラリ」形式として分類されており、実際にはより基本的な形式を使用したマクロとして実装されることが多く、他の言語に比べて実装作業がはるかに容易になります。
beginR5RSではライブラリ構文として定義されていましたが、スプライシング機能を実現するには、エキスパンダーがそれを認識している必要がありました。R6RSでは、もはやライブラリ構文ではありません。
以下の2つの表は、R5RSスキームにおける標準的な手順を示しています。R6RSははるかに広範囲にわたるため、このような概要を作成することは現実的ではありません。
一部の手続きは、言語において単一の機能に容易に分類できないため、複数の行に表示されます。
名前に「-ci」が含まれる文字列および文字プロシージャは、引数間の比較を大文字小文字を区別せずに実行します。つまり、同じ文字の大文字と小文字は等しいとみなされます。
2つ以上の引数を取る「-」と「/」の実装はR5RSで定義されていますが、オプションとなっています。
Schemeはミニマリズムを特徴としているため、多くの一般的な手続きや構文形式は標準規格で定義されていません。コア言語をコンパクトに保ちつつ拡張機能の標準化を促進するため、Schemeコミュニティは「Scheme実装要求」(SRFI)プロセスを採用しており、拡張ライブラリは拡張提案を慎重に検討することで定義されます。これによりコードの移植性が高まります。多くのSRFIは、すべての、あるいはほとんどのScheme実装でサポートされています。
さまざまな実装でかなり広くサポートされている SRFI には次のものがあります: [ 33 ]
エレガントでミニマルなデザインにより、Scheme は言語設計者、趣味家、教育者にとって人気のターゲットとなっており、典型的なインタプリタと同じくらい小さいサイズであることから、組み込みシステムやスクリプト作成にも人気があります。このため、多数の実装が生まれ、[ 34 ]そのほとんどは互いに大きく異なっているため、ある実装から別の実装にプログラムを移植するのは非常に困難であり、標準言語のサイズが小さいため、標準で移植可能な Scheme で複雑な有用なプログラムを作成することはほぼ不可能です。[ 14 ] R6RS 標準は、プログラマーへの魅力を広げるために、はるかに広い言語を規定しています。
ほぼすべての実装は、開発とデバッグのために、従来の Lisp スタイルのread–eval–print ループを提供します。また、多くは Scheme プログラムを実行可能なバイナリにコンパイルします。Scheme 実装の比較的単純さから、Cなどの言語で開発された大規模システムにスクリプト機能を追加する際の選択肢として人気があるため、他の言語で書かれたプログラムに Scheme コードを埋め込むサポートも一般的です。Gambit 、Chicken、Biglooの Scheme インタープリタは SchemeをC にコンパイルするため、埋め込みがはるかに容易になります。さらに、Bigloo のコンパイラはJava 仮想マシン(JVM)用のバイトコードを生成するように構成でき、 .NET用の実験的なバイトコードジェネレータも備えています。
一部の実装では、追加機能がサポートされています。例えば、KawaやJSchemeはJavaクラスとの統合を提供しており、SchemeからCへのコンパイラは、C言語で書かれた外部ライブラリを容易に利用できるように設計されていることが多く、SchemeソースコードにC言語コードを埋め込むことも可能です。また、PvtsはScheme学習を支援する一連のビジュアルツールを提供しています。
Scheme はいくつかの学校で広く使用されています[ 35 ]。特に、いくつかの入門コンピュータ サイエンスコースでは、教科書「Structure and Interpretation of Computer Programs (SICP)」と併用して Scheme を使用しています[ 36 ] 。過去 12 年間、PLT はProgramByDesign (旧 TeachScheme!) プロジェクトを運営しており、これにより 600 近い高校教師と数千人の高校生が基本的な Scheme プログラミングに触れてきました。MITの古い入門プログラミング クラス 6.001 は Scheme で教えられていました[ 37 ]。6.001はより現代的なコースに置き換えられましたが、SICP は MIT で引き続き教えられています[ 38 ] 。同様に、 UC Berkeleyの入門クラスCS 61A は、動的スコープを示すためにLogoに少し寄り道する以外は、2011 年まで完全に Scheme で教えられていました。今日では、MITと同様に、バークレー校もシラバスをPython 3を主に使用したより現代的なバージョンに置き換えていますが、現在のシラバスは依然として古いカリキュラムに基づいており、授業の一部は依然としてSchemeで教えられています。[ 39 ]
教科書「How to Design Programs」は、一部の高等教育機関で入門コンピュータサイエンスコースに使用されています。ウースター工科大学は、入門コース「Introduction to Program Design (CS1101)」でSchemeのみを使用しています。[ 40 ]ローズ・ハルマン工科大学は、より高度なプログラミング言語の概念コースでSchemeを使用しています。[ 41 ]ブランダイス大学の基幹コース「Structure and Interpretations of Computer Programs (COSI121b)」も、理論計算機科学者のハリー・メアソンによってSchemeのみで教えられています。[ 42 ]インディアナ大学の入門クラスC211は、完全にSchemeで教えられています。このコースの自己学習版であるCS 61ASも引き続きSchemeを使用しています。[ 43 ]イェール大学とグリネル大学の入門コンピュータサイエンスコースもSchemeで教えられています。[ 44 ] ミネソタ大学ツインシティ校の以前の入門コンピュータサイエンスコース CSCI 1901 も、主要言語として Scheme を使用し、その後、学生に Java 言語を紹介するコースがありました。[ 45 ]しかし、MIT の例に倣い、同学科は 1901 を Python ベースの CSCI 1133 に置き換えました。[ 46 ]一方、関数型プログラミングは、第 3 学期のコース CSCI 2041 で詳細に扱われています。[ 47 ]
このスキームは、以下の目的にも使用されています。
{{cite web}}: CS1 maint: 複数の名前: 著者リスト (リンク)LISP における FUNCTION と QUOTE の違いを説明するのに役立つ比喩は、自由変数が現在の環境に逃げ出すため、QUOTE を関数の多孔質または開いた被覆と考えることです。FUNCTION は閉じたまたは非多孔質の被覆として機能します (そのため、Landin は「クロージャ」という用語を使用しています)。したがって、「開いた」ラムダ式 (LISP の関数は通常ラムダ式です) と「閉じた」ラムダ式について話されます。[...] 環境問題への私の関心は、この問題を深く理解していた Landin が 1966~67 年に MIT を訪れたときに始まりました。そこで私は、
LISP
の「閉じた」ラムダ式の評価結果であるFUNARGリスト
と
ISWIM
のラムダクロージャとの間に対応関係があることに気づきました。