文字と文字列

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

 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 関数は引数がなければ標準入力から読み込むってことでいいのかな。

練習:letrec

letrec の練習。

 cf. 7. 繰り返し – もうひとつの Scheme 入門

の練習問題4より。

my-reverse

リストの要素の順番を反転させる関数 my-reverse。

(define my-reverse
  (lambda (ls)
    (letrec ((iter (lambda (l1 l2)
                     (if (null? l1)
                         l2
                         (iter (cdr l1) (cons (car l1) l2))))))
      (iter ls '()))))

(print (my-reverse '(1 2 3 4 5)))
^o^ > gosh my-reverse4.scm
(5 4 3 2 1)

sum

数のリストの要素の合計を求める関数。

(define sum
  (lambda (lis)
    (letrec ((iter (lambda (l s)
                     (if (null? l)
                         s
                         (iter (cdr l) (+ s (car l)))))))
      (iter lis 0))))

(print (sum '(1 2 3 4 5)))
^o^ > gosh sum2.scm
15

string->integer

正の整数を表す文字列を整数に変関する関数。

(define string->integer
  (lambda (str)
    (letrec ((iter (lambda (ls i)
                     (if (null? ls)
                         i
                         (iter (cdr ls) (+ (* i 10) (- (char->integer (car ls)) 48)))))))
      (iter (string->list str) 0))))

(print (string->integer "1234"))
^o^ > gosh string-to-integer2.scm
1234

letrec

letrec は局所関数を定義する一般的な方法だそうだ。

 cf. 7. 繰り返し – もうひとつの Scheme 入門

let と違って定義内で自分の名前を参照できるので、再帰関数を定義することができる。
例を示そう。↓これは以前書いた take。define を使って局所関数 f を定義している。

(define take
  (lambda (n lis)
    (define f
      (lambda (m l1 l2)
        (if (= m 0)
            (reverse l2)
            (f (- m 1) (cdr l1) (cons (car l1) l2)))))
    (f n lis '())))

これを letrec を使って書き直すとこうなる。

(define take
  (lambda (n lis)
    (letrec ((f (lambda (m l1 l2)
                  (if (= m 0)
                      (reverse l2)
                      (f (- m 1) (cdr l1) (cons (car l1) l2))))))
      (f n lis '()))))

(print (take 2 '(1 2 3 4 5)))

3行目(から6行目)で局所関数に f という名前をつけている。そして6行目でその f を再帰的に呼び出している。
実行結果:

^o^ > gosh take-letrec.scm
(1 2)

練習:名前つきlet

名前つきlet の練習。

 cf. 7. 繰り返し – もうひとつの Scheme 入門

の練習問題3より。

my-delete

リスト (ls) から要素 (x) を取り除いたリストを返す関数。

(define my-delete
  (lambda (x ls)
    (let loop ((y x) (l1 ls) (l2 '()))
      (if (null? l1)
          (reverse l2)
          (loop y (cdr l1) (if (eq? y (car l1))
                               l2
                               (cons (car l1) l2)))))))

(print (my-delete 'c '(a b c d e)))
^o^ > gosh my-delete3.scm
(a b d e)

この関数は下のように素直な再帰のほうがわかりやすいけど、代わりに末尾再帰にならない。

(define my-delete
  (lambda (x ls)
    (cond
      ((null? ls) '())
      ((eq? x (car ls)) (my-delete x (cdr ls)))
      (else (cons (car ls) (my-delete x (cdr ls)))))))

index

リスト (ls) の要素 (x) の位置を返す関数。位置は 0 から数え始め、 x がない場合は #f を返す。

(define index
  (lambda (x ls)
    (let loop ((y x) (l ls) (n 0))
      (cond
        ((null? l) #f)
        ((eq? y (car l)) n)
        (else (loop y (cdr l) (+ n 1)))))))

(print (index 'c '(a b c d e)))
(print (index 'f '(a b c d e)))
^o^ > gosh index2.scm
2
#f

sum

数のリストの要素の合計を求める関数。

(define sum
  (lambda (lis)
    (let loop ((l lis) (s 0))
      (if (null? l)
          s
          (loop (cdr l) (+ s (car l)))))))

(print (sum '(1 2 3 4 5)))
^o^ > gosh sum.scm
15

string->integer

正の整数を表す文字列を整数に変関する関数。入力エラーチェックはしなくて良い。
例: “1232” → 1232
ヒント:

  1. 文字 #\0 … #\9 のASCII コード番号から 48 を引くとその数そのものになります。 アスキーコードを求める関数は char->integer です。
  2. 文字列を文字のリストに変換する関数 string->list を使うと便利です。
(define string->integer
  (lambda (str)
    (let loop ((ls (string->list str)) (i 0))
      (if (null? ls)
          i
          (loop (cdr ls) (+ (* i 10) (- (char->integer (car ls)) 48)))))))

(print (string-&gt;integer "1234"))
^o^ > gosh string-to-integer.scm
1234

range

0 から n 未満の整数のリストを返す関数。

(define range
  (lambda (n)
    (let loop ((m (- n 1)) (l '()))
      (if (< m 0)
          l
          (loop (- m 1) (cons m l))))))

(print (range 10))
^o^ > gosh range.scm
(0 1 2 3 4 5 6 7 8 9)

ave

任意個の引数をとりそれらの平均を返す関数。レストパラメータを使う。全ての引数は数、エラー処理は不要。

(define ave
  (lambda ls
    (let loop ((l ls) (s 0) (n (length ls)))
      (if (null? l)
          (/ s n)
          (loop (cdr l) (+ s (car l)) n)))))

(print (ave 1.1 2.3 4.6))
(print (ave 3.3 4.7 10.2 20.6 100.1))
^o^ > gosh ave.scm
2.6666666666666665
27.779999999999994

(lambda ls ...) という書き方で、0個以上の引数をとりすべて ls にリストとして束縛される。以下も参照。
 cf. 可変長の引数をとる関数

名前つきlet

cf. 7. 繰り返し – もうひとつの Scheme 入門

以前 my-reverse という関数を書いた。

(define my-reverse
  (lambda (lis)
    (define rev
      (lambda (l1 l2)
        (if (null? l1)
            l2
            (rev (cdr l1) (cons (car l1) l2)))))
    (rev lis (quote ()))))

この my-reverse では rev という名前の再帰する局所関数を定義して、それを呼び出している。

名前つきlet はこの再帰する局所関数の代わりになるものだ。ループに名前をつけたものといってもいい。名前つきlet は (let name binds body) という形をしている。name がループの名前で、body の中で再帰的に呼び出すことができる。binds は普通の let と同じように局所変数とその値(ループするときの初期値になる)の組からなる。body の中で再帰するときには新たな引数を与える。
なんだかうまく説明できないけど、例を見たほうがはやいだろう。上の my-reverse を名前つきlet を使って書き直すと次のようになる。

(define my-reverse
  (lambda (ls)
    (let loop ((l1 ls) (l2 (quote ())))
      (if (null? l1)
          l2
          (loop (cdr l1) (cons (car l1) l2))))))

(print (my-reverse '(1 2 3 4 5)))

3行目の loop がループにつけた名前だ。このとき引数の l1 は ls、l2 は (quote ()) で初期化される。そして6行目で再帰的に呼び出している。新しい引数は (cdr l1) と (cons (car l1) l2) だ。
実行してみよう。

^o^ > gosh my-reverse3.scm
(5 4 3 2 1)

let – 局所変数を使う

関数内で局所変数を使いたいときには let を使って定義できる。

 cf. 6. 局所変数 – もうひとつのScheme入門

let 式は、(let binds body) という形をしている。具体例を示そう。

gosh> (let ((x 2)
            (y 3))
           (* x y))
6

これを使って同ページにある練習問題をやってみよう。

練習問題 1
Scheme 入門 4 の練習問題を1つの関数で書いてみてください。
つまり、初速度 v, 角度 a 度で投げたボールの飛ぶ距離を求める関数を書いてください。

おっと、その前に入門4の練習問題を示しておかないと。

練習問題 2
ボールを投げたときに飛ぶ距離を求める関数を以下の手順で書いてみようと思います。

  1. 角度の度を弧度法単位(ラジアン)に変換する関数。
    180 度は π ラジアンである。 π の定義は、
    (define pi (* 4 (atan 1.0)))
    を用いよ。
  2. 速度 vx で等速運動するものが t 秒間に移動する距離を求める関数。
  3. 垂直方向の初速度 vy で投げたものが落ちてくるまでの時間を 計算する関数。
    空気抵抗は無視し、重力加速度 g は 9.8 m s-2 とする。
    ヒント:落ちてくるときの速度は -vy になっているから、
    2 vy = g t
    が成り立つ。ここで t は落ちてくるのにかかる時間である。
  4. 1–3 の関数を利用して、初速度 v で角度 theta 度で投げたものが飛ぶ距離を求める関数。
    ヒント:まず、最初に関数を利用して角度 theta を弧度法単位に換算する(それを theta1 とする)。
    垂直、水平方向の初速度はそれぞれ v sin(theta1), v cos(theta1) で表される。 落ちてくるまでにかかる時間は関数3を用いて計算できる。 水平方向に加速度はかからないので、飛ぶ距離は関数2を用いて計算できる。
  5. 初速度 40 m/s, 角度 30 度で投げたボールが飛ぶ距離を上で定義した関数を用いて求めよ。 (肩の強いプロ野球選手が遠投したときの距離に近い値になります。)

この練習問題2の回答はこうなる。

(define pi (* 4 (atan 1.0)))

(define deg->rad
  (lambda (deg)
    (/ (* deg pi) 180.0)))

(define distance
  (lambda (vx t)
    (* vx t)))

(define vtime
  (lambda (vy)
    (/ (* 2 vy) 9.8)))

(define throw
  (lambda (v theta)
    (distance (* v (cos (deg->rad theta))) (vtime (* v (sin (deg->rad theta)))))))

(print (throw 40.0 30.0))

実行:

^o^ > gosh throw.scm
141.39190265868385

で、こっちが let の練習問題の回答。度で与えられた角度をラジアンに変換して局所変数 r に束縛している。

(define throw
  (lambda (v a)
    (let ((r (/ (* a 4 (atan 1.0)) 180.0)))
      (/ (* v (cos r) 2 v (sin r)) 9.8))))

(print (throw 40.0 30.0))

実行:

^o^ > gosh throw2.scm
141.39190265868385