Scheme
対象読者
[編集]このページではプログラミングのまったくの初心者、もしくは他のプログラミング言語は知っているがSchemeについて知識がないかたを主な対象者として、Schemeプログラミングを素早く習得できるように解説していきます。
Schemeの紹介
[編集]Scheme(スキーム)とはw:プログラミング言語のひとつです。 Schemeの最大の魅力はそのシンプルさにあります。おそらく(Brainf***などのジョーク言語を除けば)現存するあらゆる言語のなかでも最も言語仕様が小さい言語で、つまりはもっとも習得のしやすい言語であるといえます。また、実用にもじゅうぶん耐えうるだけの機能を持ち、プログラミングの楽しさを体験するにはまさにうってつけです。すでにC言語やPerlなどの他の言語を習得している方にとっても、プログラミングの理解を深める上で覚えておいて損はない言語です。
この項ではSchemeの知識がゼロの状態から、最低限必要なことだけを最短で理解できるように解説していきます。この項では基本的だがしかし本格的なプログラミングの概念を学び終えるのに、初学者の方でも半日とかからない分量にしています。 さあ、Schemeでプログラミングをあっという間に身に着けてしまいましょう!
Schemeは2015年現在に至るまで何度か改良が加えられ、Revised7 Report on the Algorithmic Language Scheme(R7RS)という仕様書がもっとも新しいものです。ここではR7RSに準じて説明していきます。仕様の詳細については外部リンクの項を参照してください。
処理系に触れてみる
[編集]プログラムを実行するには、その言語に対応した何らかの処理系(プログラムを処理するソフトウェア。ここではプログラムを実行するもののこと)が必要です。プログラムの構文解説に入る前に、自分でプログラムを入力して確かめることができるようにインタプリタの使い方を覚えましょう。フリーウェアとして公開されている処理系も多いので探してみましょう。処理系を探すには外部リンクの項も参照してください。
もし処理系のインストール作業が億劫であれば、Webブラウザ上でSchemeプログラムの実行を試せるサイトがあります。次の「codepad」というサイトでは、(1)ページ左のオプションボタンから、「Scheme」を選択する (2)テキストボックスにプログラムを入力する、もしくはコピーアンドペーストする (3)「Submit」ボタンを押す という手順を踏むだけで処理系のインストール作業なしにSchemeの実行を試すことができます。この項目程度の内容であれば「Codepad」でもじゅうぶん事足ります。
ためしに「codepad」を使ってみましょう。「Scheme」を選んでからテキストボックスに「(display "Hello, World!")」と入力し、「Submit」ボタンを押すと次のような結果が表示されます。
このコードを実行したところ、「Hello, World!」という文字列が出力された、ということです。他のコードを試してみる場合、「(display "Hello, World!")」の「"Hello, World!"」の部分を表示したい値に置き換えてください。たとえば、「(display (+ 5 9))」を入力すると「14」が出力されます。
他のインタプリタの大まかな使い方を説明します(以下は「Codepad」の使いかたではありません)。Schemeの処理系が入手できたら、そのヘルプにしたがって起動してみましょう。たいていのインタプリタでは、起動すると「>」記号が表示されてユーザからの入力待ちになります。
>
では、インタプリタに「(+ 1 2)」と入力してみましょう。たいていの対話式の処理系では改行すると実行します。
>(+ 1 2) 3 >
「3」が表示され、再びユーザからの入力待ちになりましたね。Schemeプログラム「(+ 1 2)」の実行結果が「3」だったということです。プログラムを入力して改行すると、入力されたプログラムが実行され、次の行に先ほどのプログラムの実行結果が表示されます。これを繰り返してSchemeプログラミングを進めていきます。
Schemeプログラムの構造、意味と評価の過程
[編集]プログラムとはおおまかに言えば命令と式の羅列です。数値計算の時にはまさに数式を書いていくのですが、プログラミング言語には、文字列から一部分を切り取ったり、リストからデータを取り出したりと、数値計算ではない式があります。下図を見ればわかるように、数学で使われる数式の表現と対応するものが多くあります。数式に対応するものをSchemeで書けば、それがまさに数値計算をするプログラムになります。
意味 | 数学の表記 | 対応するSchemeの表記 |
---|---|---|
関数の適用 | (f x y)
| |
変数の値の更新 | なし | (set! x y)
|
等値性の検査 | (= x y)
| |
関数の定義 | (define (f x y) (+ x y))
| |
乗算 | (* a b)
|
Schemeでは一貫して括弧の入れ子構造になっているのがおわかりになると思います。これがまさにSchemeの言語仕様がシンプルであるという所以です。Schemeではプログラムの実行を進めていくことを「評価」と呼びますが、これも簡単に言えば数式を変形して簡単にしていくことに似ています。たとえば、数学では次のように式を変形し、値を求めていきます。
これ以上簡単にしようがない「4」が出てきた時点で変形は終了です。これで答えが求まりました。このような変形をSchemeで表せば次のようになります。
(+ 1 (+ (- 4 2) 1)) ↓ (+ 1 (+ 2 1)) ↓ (+ 1 3 ) ↓ 4
数学の表記とは数字や記号の順番が異なりますが、何となく似た雰囲気はつかめると思います。入れ子になった括弧は、数学と同様に内側から評価してきます。プログラムといっても、数学の式を独特の表記で書き換えたようなものなのです。ただ、実際は式変形の途中経過は見せず、即座に「その式の値」つまり答えのみが出力されます。以下は実際の入力と出力の一例です。
>(+ 1 (+ (- 4 2) 1)) ←入力(プログラム、式) 4 ←出力(答え、式の値)
この式を「codepad」で試すときは「(display (+ 1 (+ (- 4 2) 1)))」と入力してください(「display」はこの式の評価結果を表示させるという意味です。詳しくは後ほど解説します)。
このように評価して値を得ることを「値が返る(値を返す、値を戻す)」、返ってきた値を「返り値(戻り値)」などともいいます。ソースコード上でいえば、「値が返る」とは「ある式を評価し、その式の部分をその値で置き換える」ことだと考えるとわかりやすいかもしれません。
式の要素
[編集]プログラムとは式そのものですから、w:ソースコードでは数字や文字列といったデータを記述していく必要があります。プログラミング言語にはそれぞれこのようなデータを書くための構文があり、このコード中に直接書かれた値を「リテラル」(literal)と呼びます。
ここでは最低限必要と思われるものだけ解説しますが、以下で紹介される以外にもいろいろな表現があります。詳しくは仕様を参照してください。また、以下の中にはまだ使い方が説明されていないものがありますが、それは後述します。
数値リテラル
[編集]Schemeでソースコード中に数値を記述するには、そのまま半角数字で表記し、これを「数値リテラル」といいます。小数は小数点をピリオドで入力し、負の数を示すマイナス記号(-)も使えます。「2004」「3.14159265358979」「-273.15」などです。ただし、入力した数値が必ずしも入力したとおりの精度で扱われるとは限りません。頭に「#e」をつけ完全な精度で扱うことを指定することもできます。
数値型は、整数型integer、有理数型ratioral、実数型real、複素数型complexの構造を持っており、右のものは、すべての左のものを含みます。
文字列リテラル
[編集]文字列の値を記述する場合は、その文字列をダブルクォーテーション"
で囲んで記述します。これを「文字列リテラル」といいます。これはソースコード上で数値や変数(後述)と区別するためであり、実際の評価にこのダブルクォーテーションが影響することはありません。たとえば、"古今"
と"東西"
というふたつの文字列をつなげると、"古今""東西"
ではなく"古今東西"
となります。また、123
は数値ですが"123"
は文字列です。さらに、"Hello."
"こんにちは。"
なども文字列です。
特別な文字を表す表現もあります。たとえば改行は\n
、タブ文字は\v
、逆スラッシュは\\
と入力します。また、これらの特別な文字は文字列リテラルの中では直接入力できません。つまり、"(改行)"
と書くと構文エラーになります(エラーにならない処理系もあります)。
リテラルを評価すると、そのリテラルそのものが示す値を返します。1
を評価すると1
、"こんにちは"
を評価すると"こんにちは"
がそのまま返ります。
文字リテラル
[編集]単一の文字を表現するには文字リテラルを使います。これは#\
に任意の一文字を続けて表記します。たとえば#\a
はa
を表します。また、スペースを#\space
、タブ文字を#\tab
で表します。
これは文字列リテラルとは扱いが異なりますので注意してください。たとえば"a"
と#\a
はどちらも画面に表示させるとa
ですが、それぞれ「一文字の文字列」と「文字」で異なるので注意してください。
真偽値リテラル
[編集]条件が真か偽かを表すには、真偽値型の値を使います。#t
は真、#f
は偽を表し、条件によって処理をわけるときなどに使います。ただし、#f
以外のすべての値は真として扱われます。これを利用すると、失敗したり無効だったときは偽を返し、それ以外は何か別の型の値を返す、といった関数を実装できます。
式が真偽値型か調べるには、関数boolean?を利用します。
コメント
[編集]ソースコード中にはコメントと呼ばれる注釈を書くことができます。コメント部分はプログラムの評価に一切関与しません。Schemeでは;
からその行末までがコメントです。
>(define hoge 10) ;ここがコメント hoge
Wikipedia:サンドボックス#ここから下に書き込んでください。 注釈を書く用途のほか、プログラムの一部分を一時的に評価しないようにするためにも使われます。もし評価して欲しくない部分を単純に削除してしまうと、あとで戻そうと思ったときに書き直さなければならず手間がかかるからです。コメントにしておけば先頭のセミコロンを削除するだけで元に戻せます。このように評価して欲しくない部分をコメントにすることをコメントアウトと呼びます。
手続き
[編集]Schemeには「手続き」(procedure)という概念があります。これは幾つかの処理を行いその結果を返すまとまりで、w:数学におけるw:関数と非常に良く似ています。たとえば、数学では「f(x,y) = x + y のとき、 f(1,2) = 3である」などといいますよね。Schemeではf(x,y) = x + y
の部分を「手続きの定義」、f
を「手続き」、手続きの評価に必要な値を受け取るx
とy
を「仮引数」と呼びます。f(1,2)
」の部分を「手続きの呼び出し」、1
と2
のように手続きの評価に使われる値を「引数」、実行した結果である3
を「返り値」といいます。
では、手続きの呼び出しを表現してみましょう(実は下記の手続きの解説には幾つか方便が含まれています。ですが、ここで詳細を解説すると難しくなりすぎるので、詳しくは後述します。手続きの定義は少し難しいので後回しにします)。
手続きはただ命令や数式の列をまとめる役目だけではありません。Schemeの豊富な機能は手続きを介して提供されているのです。また、あなたの書いたプログラムの機能を他のプログラムに公開するとき、それは手続きの定義によって行われます。Schemeの機能を呼び出すとは、用意された手続きを呼び出す構文を書くことと同義です。
手続きの文法
[編集]Schemeの手続きの呼び出しは(手続き名 引数1 引数2 ……)
という文法です。手続き名は変数です。手続きがどんな引数を取るのかは手続きによって異なります。手続き名やそれぞれの引数の間はひとつ以上の空白文字で区切らなければなりません。手続き名や引数の間に空白文字がないと、区切りが分からなくなってしまうからです。空白文字とは改行、タブ文字、半角スペースの3つのいずれかです。この構文はどんな手続きでも同じです。Schemeのほとんどの手続きは、その引数が評価されてから手続きに渡されます。
手続き呼び出しの丸括弧は数学の優先順位を示す括弧とは異なり、省略できません。このため、数学のような乗算が加算に優先する、といった優先順位はSchemeには存在しません。この仕様は記号の優先順位を覚える必要がない反面、数式を煩雑にしがちで、Schemeらしい点でもあります。手続きの呼び出しが何重にもなると括弧の数を間違いやすいです。括弧は(
と)
がきちんと対になっていなければなりませんから気をつけましょう。SchemeのようなLisp系の言語は括弧だらけになるから苦手、という人も少なくないです。
実際に試してみる
[編集]Schemeには加算をする手続き「+」が予め定義されています。さっきの構文にのっとると、数学でのはSchemeでは(+ 1 2)
と表記されます。処理系で実行して試してみましょう。できたら以外にも試してみましょう。引数の和が返ってくるはずです。
>(+ 1 2) 3 >(+ 10 50) 60 >(+ 2004 2004) 4008 >(+ -25 10) -15 >(+ 3.141592 -273.15) -270.008408
なんだか変な構文だと思われるかもしれませんが、これらの構文はS式と呼ばれ、Schemeの構文のシンプルな言語仕様を支えています。「S式」は前置記法と呼ばれるもののひとつで、手続き名にあたるものが先頭に来ます。
「前置記法」の他に「中置記法」や「後置記法」もあります。C言語などは、数学のような1 + 2
という感じの中置記法が中心ですが、3種類全ての記法が入り混じっています。これに対してSchemeでは、前置記法のみしか使わないシンプルな言語仕様になっています。
注意
[編集]幾つか手続き呼び出しに関して気をつけておくことがあります。手続きには「手続き名」「引数の数」「引数の型」「返り値の型」などの要素を持っています。たとえば、幾つ引数をとるかは手続きごとに決められており、多すぎたり少なかったりすると実行したときにw:エラーになります。ただ、たまたま「+」は引数が幾つあってもよい手続きです。
また、手続きは引数の型が決まっています。たとえば、w:加算をする手続き+
は文字列を引数に呼び出すとエラーです。
たまたま「+」は引数の順番を変えても同じ値が返ってきますが、ほとんどの手続きは引数はその順番に意味があります。たとえば、-
は1つ目の引数の値から2つめ以降の引数の値を減算する手続きなので、(- 10 5)
と(- 5 10)
の値は違います。
Schemeには予め幾つかの手続きが定義されており、ユーザは新たに手続きを定義することもできます。そのScheme処理系にどの手続きが用意されているか確かめるには、その処理系のw:ヘルプと言語仕様を確認する必要があります。
束縛
[編集]束縛とは変数に値を関連付けることです。例を見てみましょう。手続きdefineは第一引数の変数に第2引数の値を束縛します。値が束縛された変数を評価すると、その変数に束縛された値が返ります。つまり、束縛された値を取得するには、単にその変数を書きます。
>(define year 2004) year >(+ year 1) 2005
変数yearには数2004
が束縛されましたので、変数yearが評価されるとyearに束縛された値2004
が返ります。手続き+は2004
と1
の和2005
を返します。今まで説明に使ってきた手続き+も、じつはインタプリタ起動と同時に+に加算をする手続きが束縛されていたから使えるのです。手続きも一種の値として扱えるため、defineで束縛することができるのです。また、何も束縛されていない変数を評価しようとするとエラーです。
構文
[編集]上で束縛を説明しました。が、ここで疑問に思って欲しいことがあります。次の例を見てみましょう。
>(define year 2004) year >(define year 1000) year
下のdefineでは、yearはすでに2004が束縛されています。手続きは引数を評価してから渡すのですから、yearは2004を返すはずです。従って、(define year 1000)
は(define 2004 1000)
になるはずではありませんか。2004は変数ではないので、束縛はできないはずです。いや、そもそも最初のdefineではyearには何も束縛されていませんでしたから、なにも束縛されていない変数yearが評価されてエラーになるはずです。これはどうしたことでしょうか。
実はSchemeには手続き呼び出しと同じような構文でありながら、引数を評価せずに受け取る手続き呼び出しとはまったく別の式も存在します。defineは引数(のように見える部分)は評価しないのです。defineは手続きではなく、定義を行う「構文」に分類されます。
リスト
[編集]Schemeなどw:Lisp系の言語が何故これほどまでにシンプルな構文にこだわったかには訳があります。Schemeプログラムは「リスト」と呼ばれるツリー状の構造をとるようになっているのです。Lispがw:人工知能研究の分野に使われてきたのは、プログラム上でプログラムを組み立てるのが非常に容易だからです。
たとえば、(+ 1 (- 2 3))
という式は、実は次のようなリストです。
グレーの矩形はw:carとcdrの二つの区画を持つ「ペア」と呼ばれるものです。car部はそのペアが持っている値、cdr部はその次の要素を格納していると捉えることができます。car部には別のペアを格納することができます。cdr部は次の要素を示しますが、リストの終端を示すには空リストを使います。図中では()
で表されているのが空リストです。Lispプログラムはこの構造の繰り返しであり、このツリー構造を作り上げることでLispプログラムを作ることができるのです。car
やcdr
、set-car!
、set-cdr!
、list
、quote
などの手続きを使えば、Schemeプログラムをそのままリストとして扱えます。実はペアを直接作成する専用の構文もあるのですが、知らなくても今のところは構わないので、ここでは割愛します。
Schemeの重要な手続きや構文
[編集]実は、以上で大まかな文法は説明し終わりました。Schemeの文法はほとんどw:S式なのです。まだまだ説明していない概念はいくらでもあるのですが、それらの機能はすべて手続きや構文で提供されるのです。たとえば、数値の大小を比較するのも手続きですし、文字列を表示するのもまた手続きです。w:GUIw:ライブラリが提供されれば、ウィンドウを開いたりファイルを操作したり画像を表示したりするのもまた、手続きを介して行われます。
とはいえ、実際にあなたが望む機能を実現するには、どの手続きをどのように使えばいいのかわからないことでしょう。あとあなたに必要なのは、どんな手続きや構文が用意されているのか知り、具体的なプログラムを読んだり書いたりしてプログラミングの経験を積むことです。そのためには言語仕様を読めるようになる必要があります。ですが、以下でかんたんに主な手続きや構文を紹介しておこうかと思います。このほかにも有用なものがたくさんありますので、すべての機能を知るには言語仕様を参照しましょう。
cons, car, cdr
[編集]Schemeの最も重要な手続きはペアをつくり、ペアから要素を 取り出す次の3つです。
- cons ペア(dotted pair)をつくる
- car pairの最初の要素を取り出す
- cdr pairの次の要素を取り出す
consはペアを作る手続きです。
(cons 1 2)
=> 1 . 2
このペアから無限に続くリストが作れます。リストの終わりを()
という空リストで示すこととすれば、1 2 3と続くリストは、
(cons 1 (cons 2 (cons 3 '())))
と定義され、
(1 2 3)
と表現されます。
特に、Schemeでは、リスト構造が根本であり、それは、リストは再帰的(recursive)に定義される構造だからです。
(cons 1 2) => (1 . 2)
(car (cons 1 2)) => 1
(cdr (cons 1 2)) => 2
(cons 1 (cons 2 '())) => (1 2)
(car (cons 1 (cons 2 '()))) => 1
(cdr (cons 1 (cons 2 '()))) => (2)
この例だけ見ると、リストの重要性は判らないかも知れません。
四則演算
[編集]- + 加算
- - 減算
- * 乗算(「×」ではなく「*」を使います)
- / 除算(「÷」ではなく「/」を使います)
これらのw:四則演算の手続きは、整数や小数など数値型の値に適用できます。これだけでもSchemeを簡易電卓として使えそうですね。
display
[編集]displayは引数に与えられた値を出力します。プログラム実行中の途中経過を表示したりする場合にも便利です。下の例では結果が1010となっていますが、displayが表示した10とインタプリタがプログラムから得た値10が、改行していないのでつながって見えるだけです。文字列や変数も引数として使えます。
>(display 10) 1010
表示できるのは、Schemeの処理系が受け付けるすべての S式です。数値、文字、文字列、クオートされたリストなど、何でも表示できます。ただし、関数は内部表現が処理系毎に異なることに注意して下さい。(display 10)
の評価値は、未定義です。他の変数に束縛する意味はありません。複数の値を表示するとき、Scheme では以下のように書くことができます。
>(define x "Xvalue") >(for-each display `(1 2 3 x ,x "\n" "newline")) 1 2 3 x Xvalue newline
準引用により ,
が前に付くと、値が展開されます。それから
>(display 10 output-port)
と書けば、出力先を変更できます。output-port は、関数 open-output-file の返り値を指定します。
lambda
[編集]lambdaは手続きを新たに定義し、その手続きを表す値を返す構文です。Schemeは手続きであっても、数値や文字列と同じように値として処理することができます。lambda構文の第一引数には手続きの引数名となる変数のリストを渡します。第2引数以降は手続き本体となる式を渡します。ここで作成された手続きは一種の値なので、defineで束縛することができます。サンプルプログラムを示します。
> (define average (lambda (x y) (/ (+ x y) 2))) average > (average 10 20) 15
新たに定義した手続きは二つの引数をとり、その平均を返します。その手続きは変数averageに束縛しています。インタプリタは(average 10 20)
を手続き呼び出しだと判断し呼び出しました。前述では手続き名には変数を記述すると解説しましたが、実際には手続きの値をとる式を書きます。averageには手続きが束縛されているため、リストの先頭に書くことができます。従って次のようなこともできます。
> ((lambda (x y) (/ (+ x y) 2)) 10 20) 15
平均を計算する無名の手続きを定義し、返り値をリストの先頭に使いその手続きを呼び出しています。
手続きは引数や返り値にも使えるため、手続きを返す手続きも作成できます。下の例では、最初に平均を計算する手続きを返す手続きを作成し、それを変数create-average-functionに束縛しています。そして、2つめ、3つめで変数create-average-functionに束縛されていた平均を計算する手続きをつくる手続きに、まず加算と乗算の定義を与え、平均を計算する手続きを得ます。そして、その手続きに引数10と20を与えて、具体的な平均値を計算しています。
> (define create-average-function
(lambda (add multiply)
(lambda (x y) (multiply (add x y) (/ 1 2)))))
create-average-function
> ((create-average-function + *) 10 20) ; 相加平均
15
> ((create-average-function * expt) 10 20) ; 相乗平均、exptは累乗を計算する組み込み関数。
14.1421356...
quote
[編集]quoteは引数に与えられたものを評価せずにそのまま返す構文です。
>(quote (+ 1 2)) (+ 1 2) ;(+ 1 2)は評価しないと(+ 1 2)というリスト >(define hoge 10) hoge >hoge 10 ;quoteを使わなければ、変数は束縛されている値を返す >(quote hoge) hoge ;quoteを使うと、変数は変数そのものを返す
特別な構文(Schemeの機能の「構文」ではなく、一般的な意味の構文)として、'(+ 1 2)
と書くことができます。(quote (+ 1 2))
まったく同じ意味ですが、字数が少なく見やすくなります。このように、書きやすさ、見やすさのために導入された構文をw:糖衣構文(シンタックスシュガー)といいます。
関連項目
[編集]外部リンク
[編集]- R5RS
- R5RS 本家。
- アルゴリズム言語Schemeに関する第五改訂報告書 日本語訳。
- R5RS日本語訳 日本語訳。
- R6RS
- R7RS
- インタプリタとコンパイラ
- Guile GNUプロジェクトで開発されているインタプリタ。
- Gauche インタプリタ。
- Chez Scheme 商用Schemeコンパイラ。
- Petite Chez Scheme 上記Chez Schemeのフリー版インタプリタ。