今まで手続き型プログラミングばかりをやってきた自分が、抽象化とかの話しから、関数型プログラミングって、何がそんなに違いがあるのかと思って、Gaucheを初めてみた。
今まで、Cとかのソースを読んでいて、ある変数の内容で分岐をするコードを調べたりする時に、変数の中身を設定しているところを探して、該当箇所が何箇所もあって、更にその設定している関数を呼び出しているところを検索してとかやっていると、それが普通のことだと思う反面、ウンザリすることが多々あったりする。
Cとかだとマクロの定義で変数とかの名前が直接見えなかったりする場合は、更に戦意喪失しそうになる。
なので、副作用を持たない関数ってどう言うものなのかについてとても興味がある。
因みに、長くやっていることもあって、今現在、C(C++)が最も好きな言語。簡単に捨てる気もないのだけど。
現状のGauche(Scheme)プログラマレベルは、1.5位かな。
Scheme:Schemeプログラマのレベル10
まずは、今までの手続き型プログラミング言語を勉強すると時と同じように、コメント、変数定義、関数、if文、ループの書き方位までを調べることに。
なんとなく、Gauche(Scheme)の雰囲気が伝われば良いかと。
ちょっと調べて行って思ったのは、手続き型プログラミング言語の書き方と同じように調べようとしても、中々しっくりするサイトは見つけ難い。
少し理解していくと、Gauche(Scheme)の言語仕様をある程度網羅的に説明するためには、今あるサイト(主に入門サイト)で書いているような内容になるのだろうと思った。
とは言え、取っ掛かりとしては、上記で書いたような項目の並び(コメント、変数定義、関数、if文、ループ)で書くのも有りかと思うので、この記事では、その順番に記述する。
#コメント
慣れない言語でソースを書くときは、コメントで、調べようとしていることや、書こうとしているコードの内容、仮に実装しているコードとかの説明を書きたくなる。
なので、自分はいつもコメントの書き方から調べるようにしている。
##●行コメント
";"(セミコロン)の後ろがコメントになる。
;コメント
(print "hello") ;ここからコメント(行の途中からでも書ける)
因みに慣例的に、セミコロンの数で、コメントの内容を書き分けるようだ。
・コメントを書く - Gaucheクックブック
##●ブロックコメント
"#|"と"|#"で括る。ブロックコメントをネストすることもできる。が"#|"と"|#"の対応付けは必要。
#|
この行も、
この行もコメント。
#|ブロックコメントをネストすることもできるが、対応付けは必要|#
|#
##●S式コメント
S式コメントは、コメントにするS式の前に、"#;"を付けて、そのS式1つだけをコメントにする。
※S式については下記のサイトなどを参照のこと。
=>S式とconsセル
#;(begin
(print "Hello, ")
(print "world"))
上記の例の、(print "Hello, ")
だけをコメントにする場合は、次のように書く。
(begin
#;(print "Hello, ")
(print "world"))
#変数定義
変数定義の説明の前に、リストについて簡単に説明する。
Gaucheの構文は基本的にリストで表現する。リストは、空白区切りで幾つかの文字列を要素として、括弧で括る。
(print "Hello!!")
上記の例は、print
と"Hello!!"
の2つの要素を持っているリスト。
リストの先頭の要素は手続きで、2つ目以降の要素は手続きの引数。
一般化すると、
(<手続き> <引数1> <引数2> ...)
のような書き方になる。手続きは関数と言った方が理解し易いかも知れない。
リストは入れ子を持つことができ、括弧の組み合わせで、ブロックとかが表現できる。
全ての構文がリストの組み合わせで書かれる。
変数の定義についても同様で、手続きdefineにより次のように書く。
(define x 123)
上記は、変数xを数値123で定義している。
見ての通り変数の型指定がない。変数は型を持たずに、データが型を持つ。
このコードは変数(シンボル)xに数値123を結び付けていると言った方がイメージし易いかも知れない。
これをSchemeでは、変数xを数値123で束縛(bind)していると言う。
因みに上記のコードの後に、次のコードを書くと、
(define x "test")
今度は、変数xは文字列"test"に束縛されるようになる。
このように、変数名はデータとかに結び付けるラベルのようなもので、手続きを結び付けることもできる。
要するに、結び付けるものがデータであるか手続きであるかにより、変数名になったり、手続き名になったりする。
#関数
Gaucheでは手続きと言う方が一般的なのかも知れない。が、この記事では以降、関数で統一する。
まず関数の定義の例から。関数定義もdefineを使う。
(define (two-times x)
(* x 2))
上記の例だと、two-timesが関数名で、xが関数の引数になる。この関数では、xを2倍した値を返している。
実際には関数(手続き)の定義にはlambda関数を使用する。上記の書き方は、lambda関数を使った書き方を簡略化したもの。
上記をlambda関数を使った書き方にすると、次のようになる。
(define two-times
(lambda (x) (* x 2)))
この書き方だと、two-timesに(lambda (x) (* x 2))を紐付けているのがはっきりする。
ただ、lambdaは、無名関数の定義とかで結構使用するけど、関数定義には簡略化の方を使う方が、自然で一般的な気がする。
#if文
分岐を表現するためには、ifとかcondとかの関数がある。
その他、andとかorとかの関数とかでも分岐を表現することができる。こちらはCとかの論理演算子的な使い方ができる。
簡単な使用例を次に示す。
(define x 7)
(if (odd? x) (print "奇数!") (print "偶数!"))
ifの1つ目の引数は条件式を指定して、この条件式の結果が真(#t)の場合は2つ目の引数が評価され、偽(#f)の場合は3つ目の引数が評価される。
この例だと、(odd? x)
はxが奇数であるかを判定して、奇数の場合は#tを返して、そうでなければ#fを返す。この場合xは7なので、(odd? x)は#tを返し、ifの2つ目の引数である(print "奇数!")が評価される。
インタープリタで上記を実行すると、次のように表示される。
gosh> (define x 7)
x
gosh> (if (odd? x) (print "奇数!") (print "偶数!"))
奇数!
#<undef>
ifだと、条件式にマッチした場合とマッチしない場合の2つの分岐しか書けない。3つ以上の分岐を書く場合は、condを使う。
(define x 7)
(cond ((= (modulo x 3) 0) (print "3で割り切れる"))
((= (modulo x 5) 0) (print "5で割り切れる"))
(else (print " 3でも5でも割り切れない")))
andとかorは、幾つかの条件を満たすような記述をする場合に使用したりする。
例えば、aとbが共に真か、cが真の場合に、真になるような条件を書く場合に使用する。
この場合、次のような記述になる。
(if (or (and a b) c) (print "条件にマッチ"))
#ループ
繰り返しのコードを書くためにはdoとかfor-eachとか言う関数がある。
実際にはループ処理は再帰呼び出しを使用する場合が一般的。doとかも内部的には再帰呼び出しが使用されている糖衣構文のようです。
では、1から10までの数(整数)を加算するコードをCとGaucheで書いて見る。
//関数定義
long sum(long from, long to)
{
long i, s;
for (i = from, s = 0; i <= to; i++) {
s += i;
}
return (s);
}
//関数の呼び出し(main()関数の中とか)
long s = sum(1, 10);
(define (sum from to)
(if (<from to)
(+ from (sum (+ from 1) to))
from))
Gaucheの実装例だと、ループ処理は使わずに再帰呼び出しにより処理をしている。また、変数を使って代入するような操作もない。
CでGaucheの実装例と同じようなコードを書いてみると、
//関数定義
long sum(long from, long to)
{
if (from < to) {
return (from + sum(from + 1, to));
} else {
return (from);
}
}
//関数の呼び出し(main()関数の中とか)
long s = sum(1, 10);
殆ど同じようなコードが書ける。
だが、余程の理由がなければCで上記のようなコードは書かない。ループで書けるような単純な繰り返し処理を再帰呼び出しで処理すると、性能的な問題もあるし、スタックを食いつぶす可能性もある。そして何よりも分かり難い。
ところが、Gauche(Scheme)の場合はどうかと言うと、関数呼び出しのネストが深くなっても問題ないような作りになっており、メモリが許す範囲で関数呼び出しのネストができる。
更に再帰呼び出しについては、最適化(末尾再帰)により、負荷が軽減されるよう実装されている。
読み易さについてはどうか。確かに慣れないと読み難い。但し、S式と言う統一された枠組みで考えるとこのようなコードは自然に思える。
Gaucheでは、コードもデータも基本的にはリストで表現する。このため、リストを処理する組み込み関数が豊富にある。
例えば、先ほどの例のコードは、fold関数を使うと次のように書ける。
(fold + 0 '(1 2 3 4 5 6 7 8 9 10))
foldはリストから要素を1つずつ取り出して、第二引数の手続きを使って、第三引数を初期値として処理を繰り返す。
上記の例だと、手続きは+で初期値は0。リストの先頭から1つずつ要素を取り出して、
(+ 0 1)の結果=1となり、次はこの結果(=1)と次のリストの値が演算の対称となり、
(+ 1 2)の結果=3で、これを繰り返していく
(+ 3 3)の結果=6
(+ 6 4)の結果=10
(+10 5)・・・
ところで、リストに加算する値を全て書かなければならないのだと、大きな数値までの加算には使い難い。
これに対してはiota関数を使って対処できる。
gosh> (fold + 0 (iota 10 1))
55
gosh> (fold + 0 (iota 10000 1))
50005000
簡単なコードしか書いていないけど、結構使えるんじゃないのって言う気がしてくる。
慣れると、ちょっとしたコードは効率的に書けそうな気がする。
ただ、まだ触り始めて日が浅いせいもあるかも知れないけど、大規模なプログラムを作るのは今のところ疑心暗鬼。
何にしても魅力的な言語であることは間違いない。
最後に、doとfor-eachを使って1から10までの加算を行うコードを書いてみる。
gosh> (do ((i 1 (+ i 1)) (s 0 (+ s i)))
((> i 10) s))
55
gosh> (define s 0)
s
gosh> (for-each (lambda (x) (set! s (+ s x))) (iota 10 1))
#<undef>
gosh> s
55
#付録(iotaを実装してみる)
Gaucheには予約語はない。defineとかでさえも書き換えることができる。
iotaを自分で実装すると次のような感じに書ける。
gosh> (define (iota c . args)
(let loop ((c c) (n (if (null? args) 0 (car args))))
(if (> c 1)
(cons n (loop (- c 1) (+ n 1)))
(list n))))
iota
gosh> (iota 10 1)
(1 2 3 4 5 6 7 8 9 10)
gosh> (iota 10 20)
(20 21 22 23 24 25 26 27 28 29)
まだ使ったことはないけど、マクロ(define-syntaxとか)を使うと、自分で構文を作ってみたり、既にある構文をカスタマイズしたりできる。
とても面白そうです。