main手続き

今まで、Scheme のスクリプトは頭から順に実行されるもんだと思っていたけど、そうでもないらしい。main という名前の関数(Scheme では手続きというのか)が定義されていると、それがスクリプトのエントリポイントになる。

 cf. 3.3 Schemeスクリプトを書く – Gauche ユーザリファレンス

ファイルが正常にロードされたら、goshは userモジュールに ‘main’ という手続きが定義されているかどうか調べ、 定義されていればそれを呼びます。mainには、スクリプトへの引数のリストが 唯一の引数として渡されます。リストの最初の要素はスクリプトファイル名です。

試してみよう。

(define main
  (lambda (args)
    (print args)))

実行:

^o^ > gosh main.scm foo bar baz
(main.scm foo bar baz)

確かに main 手続きが実行された。

じゃあ、昨日の nend.scm を main 手続きを使って、引数をコマンドラインから渡せるように変更してみよう。

(define divmod
  (lambda (n m)
    (list (div n m) (modulo n m))))

(define mul
  (lambda (n)
    (let loop ((dm (divmod n 10)) (l '()))
      (if (= (car dm) 0)
          (apply * (cons (cadr dm) l))
          (loop (divmod (car dm) 10) (cons (cadr dm) l))))))

(define nend
  (lambda (n)
    (let loop ((x n) (i 0))
      (if (< x 10)
          i
          (loop (mul x) (+ i 1))))))

(define nend_smallest
  (lambda (n)
    (let loop ((i 1))
      (if (= (nend i) n)
          i
          (loop (+ i 1))))))

(define main
  (lambda (args)
    (print (nend_smallest (string->number (cadr args))))))
^o^ > gosh nend2.scm 2
25

^o^ > gosh nend2.scm 3
39

^o^ > gosh nend2.scm 4
77

^o^ > gosh nend2.scm 5
679

うまくいった。

ところで、今まで書いてきたみたいに手続きの外に (print ...) があるとどうなるんだろう。

(print "Hello, world.")

(define main
  (lambda (args)
    (print args)))

実行してみると:

^o^ > gosh main2.scm foo bar baz
Hello, world.
(main2.scm foo bar baz)

(print "Hello, world.") と main 手続きが両方実行されたよ。

マーティン・ガードナーの最難問

というのを見つけた。

 cf. 数学の粘度のやつ – arik_egaのノート

ある数の粘度は、すべての桁を掛けて出る答えが1桁になるまでにかかる積算の回数で表す。それぞれの桁の数を掛け算して出るのが2番目の数で、そのまた全桁の数を掛けて出るのが3番目の数…こうして1桁の数が出るまでやり、出るまでに重ねた掛け算の回数を数えるのだ。

例えば、77は粘度4だ。なぜなら1桁になるまで4回掛け算しなきゃならないからね(77-49-36-18-8)。粘度1で一番小さい数は10、粘度2で一番小さい数は25、粘度3で一番小さい数は39、粘度4の最小数は77だ。では、粘度5で一番小さい数は何?

Scheme でやってみた。特に何の工夫もなく力任せに1から順に試していって粘度が5になる数を見つけたらそれを返すだけ。

(define divmod
  (lambda (n m)
    (list (div n m) (modulo n m))))

(define mul
  (lambda (n)
    (let loop ((dm (divmod n 10)) (l '()))
      (if (= (car dm) 0)
          (apply * (cons (cadr dm) l))
          (loop (divmod (car dm) 10) (cons (cadr dm) l))))))

(define nend
  (lambda (n)
    (let loop ((x n) (i 0))
      (if (< x 10)
          i
          (loop (mul x) (+ i 1))))))

(define nend_smallest
  (lambda (n)
    (let loop ((i 1))
      (if (= (nend i) n)
          i
          (loop (+ i 1))))))

(print (nend_smallest 5))

実行結果:

^o^ > gosh nend.scm
679

トリボナッチ数をSchemeで

昨日は Python でやったので、今日は Scheme で。

(define tribonacci
  (lambda (n)
    (let loop ((m n) (l '(0 0 1)))
      (if (= m 0)
          '()
          (cons (car l) (loop (- m 1) (append (cdr l) (list (apply + l)))))))))

(print (tribonacci 20))

今回は名前つきletで再帰。
実行:

^o^ > gosh tribonacci.scm
(0 0 1 1 2 4 7 13 24 44 81 149 274 504 927 1705 3136 5768 10609 19513)

eq?とeqv?とequal?

違いがよくわからなかったのでメモ。

6.2 等価 – Gauche ユーザリファレンス によると次のような違いがある。

eq?

最も高速で、細かい区別ができる述語。 2つの引数がアロケートされる同じ型のオブジェクトで、かつ両者がメモリ上の全く同じ場所を占めるオブジェクトを指している場合に #t を返す。2つの引数がともに文字、あるいは数値だった場合の振るまいは Scheme の標準では定められていない。

gosh> (eq? 'Foo 'Foo)
#t
gosh> (eq? "Foo" "Foo")
#f
gosh> (eq? #\a #\a)
#t
gosh> (eq? 123 123)
#t
gosh> (eq? '(Foo) '(Foo))
#f

Gauche の場合には文字列では #f になるけど、数値では #t になるな。

eqv?

2つの引数 obj1 と obj2 がともに正確な数値、あるいはともに(NaN以外の)不正確な数値である場合、(= obj1 obj2)が真であれば #t が、偽であれば #f が返される。obj1 と obj2 がともに文字である場合、(char=? obj1 obj2)が真であれば #t が、偽であれば #f が返される。それ以外の場合は、Gaucheでは eqv? は eq? と同じ。

gosh> (eqv? 'Foo 'Foo)
#t
gosh> (eqv? "Foo" "Foo")
#f
gosh> (eqv? #\a #\a)
#t
gosh> (eqv? 123 123)
#t
gosh> (eqv? '(Foo) '(Foo))
#f
gosh>

eq? と同じじゃん。正確な数値とか不正確な数値とかがわかんないけど。

equal?

2つに引数 obj1 と obj2 がリストやベクタなどの複合型である場合、equal? は再帰的に対応する要素同士を equal? で比較する。そうでなければ equal? は eqv? と同じようにふるう。

gosh> (equal? 'Foo 'Foo)
#t
gosh> (equal? "Foo" "Foo")
#t
gosh> (equal? #\a #\a)
#t
gosh> (equal? 123 123)
#t
gosh> (equal? '(Foo) '(Foo))
#t

eq? や eqv? では #f になった文字列やリストでも #t になる。

まとめ

表にまとめると次のようになる。

オブジェクト eq? eqv? equal?
シンボル #t #t #t
文字列 #f #f #t
文字 #t #t #t
数値 #t #t #t
リスト #f #f #t

比べるのがシンボルの場合には eq?、文字の場合には eqv?、数値の場合には(上には出てこないけど)=、それ以外は equal? がいいみたいだ。

シンボル型

シンボル型とは、「一言で言うとシンボルとは、文字列をアドレスで管理するデータ型」らしい。

 cf. 12. シンボル型 – もうひとつの Scheme 入門

Ruby のシンボルと同じようなものだと考えてよさそうだ。
シンボルを比較するには、アドレスを比較する eq? が使えるので、高速に比較することができる。なので、連想配列やハッシュ表のキーに使うといいようだ。

今までのエントリに出てきた中では、数値以外のアトムが実はシンボルだった。シンボルを使うにはクォートしてやらないといけない。そうでないと Scheme は変数として評価しようとする。

gosh> foo
*** ERROR: unbound variable: foo
Stack Trace:
_______________________________________
gosh> 'foo
foo

文字列とは違うことに注意。

gosh> (symbol? 'foo)
#t
gosh> (symbol? "foo")
#f

シンボルと文字列は相互に変換できる。文字列からシンボルへは string->symbol、シンボルから文字列へは symbol->string だ。

gosh> (string->symbol "Foo")
Foo
gosh> (symbol->string 'Foo)
"Foo"

処理系にもよるけど、文字列をシンボルに変換するとき小文字にしておかないと、同じつづりでも同じアドレスにならないらしい。

gosh> (eq? (string->symbol "Foo") 'Foo)
#t

同じアドレスでないと、これが #f になるわけだな。Gauche の場合は同じになるようだ。

文字と文字列

今日は文字と文字列について。
参考ページ:

 cf. 11. 文字、文字列 – もうひとつの Scheme 入門
 cf. 6.10 文字 – Gauche ユーザリファレンス
 cf. 6.12 文字列 – Gauche ユーザリファレンス

文字

文字のリテラルは #\ に続けて文字を書く。たとえば a という文字を表すには #\a とする。なんか変な感じ。

gosh> #\a
#\a

あと、いくつかの特殊文字などには名前がついている。名前は大文字と小文字を区別しない。

  • #\space
  • #\newline
  • #\return
  • #\tab
  • #\delete
  • #\null

以下、文字にかんする関数をいくつか。

(char? obj) は obj が文字なら真を返す。

gosh> (char? #\a)
#t

(char=? c1 c2) は文字が等しければ真を返す。

gosh> (char=? #\a #\a)
#t

(char->integer c) は文字を整数(ASCIIコード)に変換する。(integer->char n) はその逆。

gosh> (char->integer #\a)
97
gosh> (integer->char 97)
#\a

(char-alphabetic? c)(char-numeric? c)(char-whitespace? c)(char-upper-case? c)(char-lower-case? c) はそれぞれ、c がアルファベット、数字、空白文字、大文字、小文字のときに真を返す。

gosh> (char-alphabetic? #\a)
#t
gosh> (char-numeric? #\1)
#t
gosh> (char-whitespace? #\space)
#t
gosh> (char-upper-case? #\A)
#t
gosh> (char-lower-case? #\a)
#t

(char-upcase c)(char-downcase c) はそれぞれ、大文字、小文字に変換する。

gosh> (char-upcase #\a)
#\A
gosh> (char-downcase #\A)
#\a

文字列

文字列のリテラルは、たとえば “abc” みたいに ” でくくる。こっちは普通な感じ。

(string? s) は s が文字列なら真を返す。

gosh> (string? "foo")
#t

(string-length s) は文字列の長さ。

gosh> (string-length "foo")
3

(string=? s1 s2) は s1 と s2 が等しいとき真を返す。

gosh> (string=? "foo" "foo")
#t

(string-ref s idx) は s の idx番目の文字を返す。idx は0から。

gosh> (string-ref "abcde" 3)
#\d

(substring s start end) は start番目 から end – 1 番目の部分文字列を返す。Python のスライスと同じ要領だな。

gosh> (substring "abcde" 2 4)
"cd"

(string-append s1 s2) は文字列の連結。

gosh> (string-append "foo" "bar")
"foobar"

(string->list s)(list->string ls) は文字列とリストの変換。

gosh> (string->list "abc")
(#\a #\b #\c)
gosh> (list->string '(#\a #\b #\c))
"abc"

こんなところか。もっと詳しくは参考ページで。

練習問題

最後に練習問題をやっておこう。11. 文字、文字列 – もうひとつの Scheme 入門 より。

単語の初めを大文字にする関数 title-style を書いてください。

(define title-style
  (lambda (str)
    (letrec ((u (lambda (l acc)
      (cond
        ((null? l) (list->string (reverse acc)))
        ((char-whitespace? (car l)) (u (cdr l) (cons (car l) acc)))
        (else (d (cdr l) (cons (char-upcase (car l)) acc))))))
          (d (lambda (l acc)
            (cond
              ((null? l) (list->string (reverse acc)))
              ((char-whitespace? (car l)) (u (cdr l) (cons (car l) acc)))
              (else (d (cdr l) (cons (car l) acc)))))))
                (u (string->list str) '()))))

(print (title-style "one size fits all"))

実行結果:

^o^ > gosh title-style.scm
One Size Fits All

練習:ファイルへ出力

ファイルへの出力の練習として、

 cf. 9. 入出力 – もうひとつの Scheme 入門

の練習問題2 と3 をやってみた。

練習問題2

ファイルをコピーする関数 (my-copy-file) を書いてください。

(define my-copy-file
  (lambda (infile outfile)
    (let ((in (open-input-file infile))
          (out (open-output-file outfile)))
      (let loop ((c (read-char in)))
        (if (eof-object? c)
            (begin
              (close-input-port in)
              (close-output-port out))
            (begin
              (display c out)
              (loop (read-char in))))))))

実行例:

^o^ > type sample.txt
Hello world!
Scheme is an elegant programming language.

^o^ > gosh -I.
gosh> (load "my-copy-file.scm")
#t
gosh> (my-copy-file "sample.txt" "sample.out")
#<undef>
gosh> (exit)

^o^ > type sample.out
Hello world!
Scheme is an elegant programming language.

練習問題3

任意個の文字列の引数をとり、それらを標準出力に1行に1つずつ出力する関数 print-lines を書いてください。

(define print-lines
  (lambda args
    (let loop ((ls args))
      (if (pair? ls)
          (begin
            (display (car ls))
            (newline)
            (loop (cdr ls)))))))

実行例:

gosh> (load "print-lines.scm")
#t
gosh> (print-lines "foo" "bar" "baz")
foo
bar
baz
#<undef>

ファイルへ出力

昨日はファイルからの入力を覚えたので、今日はファイルへの出力。

 cf. 9. 入出力 – もうひとつの Scheme 入門

open-output-file、close-output-port

(open-output-file filename) は filename を出力用に開いてポートを返す。使い終わったら (close-output-port port) でポートを閉じる。
出力は display。newline で改行する。

(define write-to-file
  (lambda (file-name message)
    (let ((p (open-output-file file-name)))
      (begin
        (display message p)
        (newline p)
        (close-output-port p)))))

実行例:

gosh> (load "write-to-file1.scm")
#t
gosh> (write-to-file "hello1.txt" "Hello, world.")
#<undef>
gosh> (exit)

^o^ > type hello1.txt
Hello, world.

call-with-output-file

(call-with-output-file filename procedure) は filename を出力用に開いて procedure を評価する。procedure はポートを引数に取る関数。

(define write-to-file
  (lambda (file-name message)
    (call-with-output-file file-name
      (lambda (p)
        (begin
          (display message p)
          (newline p)
          (close-output-port p))))))

実行例:

gosh> (load "write-to-file2.scm")
#t
gosh> (write-to-file "hello2.txt" "Hello, scheme.")
#<undef>
gosh> (exit)

^o^ > type hello2.txt
Hello, scheme.

with-output-to-file

(with-output-to-file filename procedure) は filename を標準入力として開き、procedure を評価する。procedure は引数なしの関数。終わったらポートは勝手に閉じられる。

(define write-to-file
  (lambda (file-name message)
    (with-output-to-file file-name
      (lambda ()
        (begin
          (display message)
          (newline))))))

実行例:

gosh> (load "write-to-file3.scm")
#t
gosh> (write-to-file "hello3.txt" "Hello, world. This is scheme.")
#<undef>
gosh> (exit)

^o^ > type hello3.txt
Hello, world. This is scheme.

練習:ファイルからの入力

ファイルからの入力の練習。

 cf. 9. 入出力 – もうひとつの Scheme 入門

の練習問題1。

ファイルの内容を1行ずつのリストにして返す関数 read-lines を書いてください。 hello.txt に適用すると次のようになるようにして下さい。’\n’ は #\Newline です。 改行文字は残すようにしてください。

(read-lines “hello.txt”) ⇒ (“Hello world!\r\n” “Scheme is an elegant programming language.\r\n”)

(define read-lines
  (lambda (file-name)
    (let ((p (open-input-file file-name)))
      (let loop ((lines '()) (line '()) (c (read-char p)))
        (cond
          ((eof-object? c)
           (begin
             (close-input-port p)
             (reverse lines)))
          ((eq? c #\Newline) (loop (cons (list->string (reverse (cons c line))) lines) '() (read-char p)))
          (else (loop lines (cons c line) (read-char p))))))))

実行例:

^o^ > gosh -I.
gosh> (load "read-lines.scm")
#t
gosh> (read-lines "sample.txt")
("Hello world!\r\n" "Scheme is an elegant programming language.\r\n")

ファイルからの入力

ファイルからの入力の仕方。

 cf. 9. 入出力 – もうひとつの Scheme 入門

より。

open-input-file、read-char、eof-object?

open-input-file はファイルを開いてファイルからの入力ポートを返す。read-char はポートから1文字読み込む。ファイルの終わりに達すると、eof-object を返すので、eof-ofject? でチェックする。

次の例は、sample.txt からファイルの内容を読み込む read-file 関数。

(define read-file
  (lambda (file-name)
    (let ((p (open-input-file file-name)))
      (let loop ((ls '()) (c (read-char p)))
        (if (eof-object? c)
            (begin
              (close-input-port p)
              (list->string (reverse ls)))
            (loop (cons c ls) (read-char p)))))))

3行目で入力ポートを開いて変数 p を束縛している。4行目で p から1文字読み込んで、5行目で eof-object 可動化のチェック。もしファイル終端ならポートを閉じて(7行目)文字列を返し、そうでないなら再帰(9行目)している。
(begin ...) というのは、複数のS式を順に評価して最後のS式の値を返すものらしい。

実行例:

^o^ > type sample.txt
Hello world!
Scheme is an elegant programming language.

^o^ > gosh -I.
gosh> (load "read-file1.scm")
#t
gosh> (read-file "sample.txt")
"Hello world!\r\nScheme is an elegant programming language.\r\n"

call-with-input-file

(call-with-input-file filename procedure) というかたちをしていて、procedure は入力ポートを引数にとる関数。エラー処理をしてくれるのでこちらのほうが便利、と書いてある。
call-with-input-file を使って read-file 関数を書くと次にようになる。

(define read-file
  (lambda (file-name)
    (call-with-input-file file-name
      (lambda (p)
        (let loop ((ls '()) (c (read-char p)))
          (if (eof-object? c)
              (begin
                (close-input-port p)
                (list->string (reverse ls)))
              (loop (cons c ls) (read-char p))))))))

ファイルから文字を読み込んで返すという本質的なところは read-file1.scm とおなじ。違うのは、let で入力ポートを束縛する代わりに、ポートを引数に取る関数を使っているところ。

実行例:

gosh> (load "read-file2.scm")
#t
gosh> (read-file "sample.txt")
"Hello world!\r\nScheme is an elegant programming language.\r\n"

with-input-from-file

with-input-from-file は、ファイルを標準入力として開く。call-with-input-file と同じように (with-input-from-file filename procedure) というかたちをしているけど、procedure は引数をとらない。つまり入力は標準入力に固定されるってわけだな。入力ポートは勝手に閉じられる。

(define read-file
  (lambda (file-name)
    (with-input-from-file file-name
      (lambda ()
        (let loop ((ls '()) (c (read-char)))
          (if (eof-object? c)
              (list->string (reverse ls))
              (loop (cons c ls) (read-char))))))))

実行例:

gosh> (load "read-file3.scm")
#t
gosh> (read-file "sample.txt")
"Hello world!\r\nScheme is an elegant programming language.\r\n"

これが一番使いやすそうだ。

[追記]
最後の例では read-char を引数なしで使っている。read-char 関数は引数がなければ標準入力から読み込むってことでいいのかな。