m.hana
ごあいさつ
はじめまして。プログラマですと言いたくても言えないレベルの会社員です。 まともに使える言語はJavaしかありませんが、いろんな言語をかじっては挫折、を繰り返しつつ学生時代になんとなく本を読んだLispにも手を出してみてそうこうするうちにSchemeを知り、Practical Scheme経由でこちらにきました。SICPをオンラインでみて、これはすごい本だと思ってSchemeの勉強をしたりしていますが、あんまり進んでません。SICPはSchemeですが、これをJavaやほかの言語でやったらどうなるんだろうということを考えつつ、その逆、Scheme以外で説明されているものをSchemeでやったら勉強になるかもということで試しに http://www.sra.co.jp/people/aoki/ にある「Smalltalkerのための初等数学」を教材にやってみることにしました。 試行錯誤が誰かの役に立つかもと思ったのでここにかかせてもらいます。おかしなところが多々あると思いますのでお気軽につっこんでください。(これも途中で挫折するかも...) PLTScheme使ってます。-> Gauche入れました(2006/06/10 23:42:17 PDT)
(2006/06/05 04:11:48 PDT)
5. remove
WiLiKiとかその他色々みていると自分のレベルの低さがいやになりますが、とはいえだんだんわかってきている気もしますが、読んだことをすぐに忘れてしまうのでこまります。しかし、以前読んでさっぱりわからなかったなんでもλがちょっとわかるようになっていてうれしかったりもします。
さて、removeですが、これは引数と等しい要素を削除するという関数です。Smalltalkのほうでは、集合の要素を順番に調べて一致したら削除するということをしています。うーん。最近知った事によるとGaucheにはremove!というのがあるようですが、せっかく作ったrejectが使えそうなのでこれを使ってみます。
(define-syntax remove-from-set! (syntax-rules () ((remove-from-set! set obj) (let ((result (reject set (lambda (each) (equal? each obj))))) (set! set result)))))
これもマクロじゃないとできないですね。
テスト。
(define (test-remove-from-set) (begin (define set1 (make-set 1 2 3)) (define set2 (make-set 2 3)) (define set3 (make-set 4 5 6)) (define set4 (make-set 4 5 6)) (remove-from-set! set1 1) (assert-true "(1 2 3)から1を除いたら(2 3)" (eq-set? set2 set1)) (remove-from-set! set3 10) (assert-true "(4 5 6)から10を除いたら(4 5 6)" (eq-set? set4 set3)))) (test-remove-from-set)
まだGaucheの機能をまったく使ってないです。addもremoveも名前がそのままだと何にaddやremoveなのかをわかるような名前を付けていますが、オブジェクト指向だとクラスが名前空間的に使えるのでそっちの方がいい気もします。Gaucheだとモジュールとか使うんでしょうか。2006/06/24 10:16:17 PDT
やっとGauche入れました。夜中に入れたのですが、compile.cのところで妙に時間がかかったのでそのまま寝たら朝には終わってました。installでエラーが出てあれ?と思ったらsudoしてなかった。動かしてみるとおお、早い。エディタは何にしようかな。Eclipseは遅いしなー。(2006/06/10 23:42:17 PDT)
4. add
変なことを考えてました。m.hana:迷想
いよいよ集合に要素を追加するaddを作ります。addじゃよくわからないのでadd-to-setという名前にします。 これは簡単だ。なんたって僕たちにはconsがある。楽勝だ!と思っていたのですがはまりました。挫折しそうになりました。
初めは意気揚々と下のように作りました。
(define (add-to-set set obj) (if (not-includes? set obj) (cons obj set)))
集合は重複する要素を含まないということですので含まれないかどうかチェックしてからconsします。書いてからふと。元のsetにconsされへんがな。と思いました。どうすればいいんだろう。リスト以外の何かにしなければいけないんだろうか...。うーんベクタとかあったなーとか考えながらR5RS(の翻訳版)をみてみたらベクタに値をセットする関数がありました。これだと思ってベクタを使うようにmake-setとかincludes?とかeach-elementとか全部書き直してからadd-to-setを書こうとしたら、あれ?ベクタって長さかわらないの?JavaのVectorと違うのか。ということは元のやつより大きいやつを作ってコピーして追加する要素をセットして、とかやってたら結局もとの集合に追加できないではないかということに気づきました。でも新しいやつを元のやつにset!すればいいんだな、ということはリストを使うやり方のほうがいいじゃないかと思ってまたmake-setとかincludes?とかeach-elementとか全部作り直しました。
(define (add-to-set set obj) (if (not-includes? set obj) set! set (cons set obj)))
できた。確認。あれ?元の集合がかわってない...。あ、Schemeは値渡しか。じゃあどうすればいいんだ。完全に行き詰まりました。やめようかと思いました。しかし、じゃあset!とかvector-set!とかはどうやってるんだろうと考えていてぴーんときたのが「マクロか?」ということです。そうだマクロだと思いましたがマクロって何?というレベルなので独習Scheme三週間をみました(なぜかR5RSではない)。define-macroか。やってみたら怒られました。よく見ると実装していない処理系もあるようでPLTSchemeはそのひとつのようでした。define-syntaxというのをみたことがあるのでR5RSのマクロの節をみてみるとlet-syntaxの説明しか書いてません。構文定義の節をみると、あ、あった。しかしサンプルコードがlet-syntax使ってる...。全然書き方がわかりません。let-syntaxとおんなじ書き方かと思ってやってみたら違うしあーわからーんと思いながらいろんなことをしてやっとエラーが出なくなりました。
(define-syntax add-to-set! (syntax-rules () ((add-to-set! set obj) (if (not-includes? set obj) (set! set (cons obj set))))))
これを書いた後にR5RSの下の方に、いろんなマクロが書いてあるのを見つけました...。テストします。
(define (test-add-to-set) (define set1 (make-set)) (assert-equals "set1が空ならOK" '() set1) (add-to-set! set1 1) (assert-equals "set1は(1)のはず" (make-set 1) set1) (add-to-set! set1 1) (assert-equals "set1は(1)のはず" (make-set 1) set1)) (test-add-to-set)
通りました。あー苦労した。今更ながら自分の学習の効率の悪さと頭の悪さがいやになりました(文章自体から頭の悪さがにじみでてますね)。素直にSICPをやった方がいいのかな...。(2006/06/09 18:35:20 PDT)
- SICPってどうやってたのかな、と見たら素直にconsしてました。トップレベルで副作用が生じるのがSchemerは嫌いなんでしょう。「Scheme and the Art of Programming」MITPressのp236からにSchemeで集合表現を扱った例があります。私はちゃんとやったことがないのでわからないんですけど。 sasagawa
- SICPでも集合やってましたっけ。2章の途中で止まってるし、読んだところもよく覚えてないです。ちょっと見直してみます。sasagawaさんはSchemeで数学をやってらっしゃいますね。難しくてよくわかりませんが。私が学生の頃はプログラム言語で数学をやるなんて思いつきもしませんでした。Mathematicaをみたときの衝撃はすごかった。m.hana
- horii(2006/06/11 20:24:05 PDT):
上の add-to-set! だと、obj が2回評価されるので、obj が副作用のある式だったりすると、まずいです。
gosh> (define s (make-set)) s gosh> (add-to-set! s (display 1)) 11(#<undef>)
let を使うと、obj の評価を1回にできます。(define-syntax add-to-set! (syntax-rules () ((add-to-set! set obj) (let ((obj* obj)) (if (not-includes? set obj*) (set! set (cons obj* set)))))))
- m.hana(2006/06/12 01:44:46 PDT):何回評価されるかなんか考えてもなかったです。そういうことも気をつけなきゃいけないんですね。ありがとうございます。
- m.hana(2006/06/12 03:21:08 PDT):あ、そういえばふと疑問に思ったのですが、純粋な関数型言語は代入がないそうですが、そういう言語だとどうするんでしょうか。
3. =
Scheme:Schemeプログラマのレベル10によるとレベル1.5ぐらいか...。
今回は二つの集合が同じかどうかを比較する関数です。Smalltalkの方では=メソッドをオーバーライドしていましたが、Schemeでは=という名前はまずいですよね。ちょっと考えてeq-set?という名前にしました。アルゴリズムは、お互いの要素の中で他方に含まれない要素がないことを調べるということです。SmalltalkのコードをそのままSchemeで置き換えてみます。
(define (eq-set? set another) (and (null? (reject set (lambda (each) (includes? another each)))) (null? (reject another (lambda (each) (includes? set each))))))
なんか、rejectというメソッドがありますが、これは親クラスCollectionかそのまた親クラスか...にあるメソッドのようです。こっちにはないので作ります。コードから想像するに、相手の集合にない要素を集めてくるメソッドのようです。つまり、ある集合に対してある条件に合わない要素の集合、ということですか。なんかややこしいですが、集合の要素を順に調べて条件に合わなかったらそれを結果の集合に追加していけばいいということですね。集合の要素を順に調べる、ということは前に作ったeach-elementが使えそうなのでそれを使います。eac-elementに渡すクロージャで条件に合わないやつを集める仕事をさせます。
(define (reject set cond) (let ((result '())) (begin (each-element set (lambda (each) (if (not (cond each)) (set! result (cons each result))))) result)))
condという名前を使ってしまった。(set! result (cons result))っていうのもなんかひっかかる。とりあえずテストします。
(define (test-reject) (define set1 (make-set 0 1 2 3 4)) (assert-equals "(0 1 2 3 4)から2未満を除外すると(4 3 2)のはず" (make-set 4 3 2) (reject set1 (lambda (each) (if (< each 2) #t #f)))) (assert-equals "(0 1 2 3 4)から5を除外すると(4 3 2 1 0)のはず" (make-set 4 3 2 1 0) (reject set1 (lambda (each) (equal? each 5))))) (define (test-eq-set) (define set1 (make-set 1 2 3)) (define set2 (make-set 2 3 1)) (define set3 (make-set 2 3)) (define set4 (make-set 1 2)) (assert-true "(1 2 3)と(2 3 1)は同じ集合" (eq-set? set1 set2)) (assert-true "(1 2 3)と(2 3 1)は同じ集合" (eq-set? set2 set1)) (assert-true "(1 2 3)と(2 3)は違う集合1" (not (eq-set? set1 set3))) (assert-true "(1 2 3)と(2 3)は違う集合2" (not (eq-set? set3 set1))) (assert-true "(1 2 3)と(1 2)は違う集合1" (not (eq-set? set1 set4))) (assert-true "(1 2 3)と(1 2)は違う集合2" (not (eq-set? set4 set1)))) (test-reject) (test-eq-set)
通りました。(0 1 2 3 4)から2未満を除外すると(2 3 4)じゃなくて(4 3 2)...。かっこわるい。まあ集合は順番関係ないからいいことにします。rejectのlet内にあるbeginはいらないような気がするのでとってテストをしてみると通りました。defineとかletの中は順番に評価されるんですね。
などと一発で通ったように書いてますがほんとはそんなわけはありません。eq-set?のテストで引数の順番をかえてテストしてますがなんと片方しか通らなかったのです。テスト書いてるときはまさかな、と思いながら書いてましたがそのまさかがおこりました。テスト書いててよかった。 2006/06/07 06:58:26 PDT
2.テスト
さっそくいろいろアドバイスをいただいています。ありがとうございます。アドバイスをみて「書いた覚えがない文章がある...」などと思ってしまいました。ぼけてます。
最初にはじめは集合ですとか書いてしまいましたが「Smalltalkerのための初等数学」には集合しか記事がありませんので最後まで集合です。ということでいままで作った分のテストをしようと思います。適当に関数を実行して結果を目で見て確認、ということをやっていたのですがなんどもやっているとめんどくさいのでテスト用の関数を作ることにしました。で、Schemeにもテスト用のフレームワークとかあるのでしょうが、自前で簡単なユーティリティを作ってみました。
(define (assert-equals msg expected actual) (if (not (equal? expected actual)) (begin (display "失敗! assert-equals ") (display msg) (display " 期待値=") (display expected) (display " 実際の値=") (display actual) (newline)))) (define (assert-true msg value) (if (not value) (begin (display "失敗! assert-true ") (display msg) (newline))))
失敗したときだけ失敗したことを表示します。includes?のところでequals?を使うのはよろしくないと指摘されてますが、ここでもassert-equalsにequal?を使っているのがいかにもだめっぽい。とりあえずincludes?のテストをします。
(define (test-includes) (begin (define set1 (make-set 1 2 3)) (define set2 (make-set 4 5 6)) (define set3 (make-set 3 '(4) 5)) (assert-true "(1 2 3)は1を含むはず" (includes? set1 1)) (assert-true "(4 5 6)は (4)を含まない" (not (includes? set2 '(4)))) (assert-true "(3 (4) 5) は(4)を含む" (includes? set3 '(4)))))_ (test-inludes)
defineを使ってますが普通はletなんでしょうか。letとdefineは何か違うのかな。テストケースはあれですが、とりあえずテストは通ったようです。
妖怪キートップはずし(息子)にキートップをはずされそうなのでこの辺で。 2006/06/06 04:43:15 PDT
- Rui: Gaucheに含まれるtestモジュール(GaucheRefj:gauche.test)を使うと上記の単体テストは次のように書けます。
(test* "(1 2 3)は1を含むはず" #t (includes? (make-set 1 2 3) 1)) (test* "(4 5 6)は (4)を含まない" #f (includes? (make-set 4 5 6) '(4))) (test* "(3 (4) 5) は(4)を含む" #t (includes? (make-set 3 '(4) 5) '(4)))
- m.hana(2006/06/06 16:15:53 PDT)Gaucheは元々そういうモジュールをもってるんですね。Gaucheにあこがれつつ、DrSchemeのエディタになれてしまったのでそのまま使い続けています。インデントを自動でやってくれたり括弧の対応がわかりやすいので...。Emacsでもできるんでしょうけどキーバイディングが覚えられないんですよね(Vi派)。DeSchemeは遅い(特に起動が)のでなんとかGaucheに移行したいと思います。
- emacsがいまひとつ好きになれない方(私も)にはeclipse + scheme script + gaucheなんてのも良いかと思いますよ〜
- トップレベルのdefineと手続きの先頭に書くdefineとは明確に区別され、意味が異なります。後者のdefineはinternal defineといいます。m.hanaさんがtest-includesのあとに書いたset1, set2, set3を定義するdefineはinternal defineです。internal defineはletrec (GaucheRefj:letrec)と同じなので、次のように書いたのと同じ意味になります。
(define (test-includes) (letrec ((set1 (make-set 1 2 3)) (set2 (make-set 4 5 6)) (set3 (make-set 3 '(4) 5))) (assert-true "(1 2 3)は1を含むはず" (includes? set1 1)) (assert-true "(4 5 6)は (4)を含まない" (not (includes? set2 '(4)))) (assert-true "(3 (4) 5) は(4)を含む" (includes? set3 '(4)))))
tesst-includesの本体全体を囲むbeginはとくに必要ありませんね。- m.hana(2006/06/06 16:15:53 PDT) なるほど。letrecと同じですか。なるほどといいながらletrecはまだよくわかってません。勉強します。beginはいらないんですか? いついるんだろう...。
1.集合-データ構造とか
ということで、はじめは「集合」です。最初にパッケージを作ったり名前空間を作ったりクラスを作ったりしてますが、全部飛ばして、メソッドnewからいきます。集合をどう作るかですが、Schemeですのでリストを使います。というかそれしか知りません。SICPのmake-ratにならって
(define make-set list)
としました。SICP読んでなかったらこんなこと思いつきもしなかったでしょうね。メソッドnew:sizeは作りません。initializeも作りません。 次は集合の中に、ある要素が含まれているかどうかを返すメソッドinclude:です。集合の中を調べるわけですから、集合の各要素に関数を適用するような関数があれば便利かも、と思いそのようなやつを作るというXPの精神(必要かどうかわからないものは作るな)に反することをしする私です。それが以下の関数、each-elementです。集合とクロージャを引数にとります。
(define (each-element set closure) (define (loop rest) (if (not (null? rest)) (begin (closure (car rest)) (loop (cdr rest))))) (loop set))
これができたところで、関数includes?を作ります。が、要素が含まれていることがわかったらその場で#tを返せばいいのだから、というか返したいので全部の要素を調べる必要はありません。each-elementでそんなことできるのか?しばらく考えましたができそうにないので結局each-elementは使いませんでした。XPの教えは正しかったのです(別のところで使いましたが)。ということでincludes?は次のようにしました。集合setのcarが引数anObjに等しいかどうかを調べていきます。局所的な関数loopで再帰してます。
(define (includes? set anObj) (define (loop rest) (if (null? rest) #f (if (equal? (car rest) anObj) #t (loop (cdr rest))))) (if (= 0 (set-size set)) #f (loop set)))
(define set-size length)
ちなみにeq?とeqv?とequal?の違いがいまだによくわかりません。carとかcdrが出てくるのがいやですが(抽象化できてないっぽいので)、目をつぶります。
- equal? は Java の equals と同じと考えればいいと思います。
それに対して eqv? は == に対応します。ただし、Scheme では数値や文字が
Java の BigInteger のようなもので実装されることがあるので、
それ以外の即値オブジェクト(シンボル、真偽値、空リスト、ペア、手続き等々)を
効率よく比較するために eq? が用意されています。
- おお。なるほど。すごくよくわかりました。なんとなくそんな感じがしてたのですが、どれがJavaの何に対応するのかよくわからなかったのです。ありがとうございます。m.hana (2006/06/05 16:16:47 PDT)
次は要素が集合に含まれないかどうかを返すnot-includes?です。これはincludes?を使えば簡単にできました。
(define (not-includes? set anObject) (not (includes? set anObject)))
ということで今日はこのぐらいで。 2006/06/05 06:59:29 PDT
- includes?はsetが空集合か否かのチェックを2箇所(loopを呼ぶ前とloopの中)でしているのでよろしくないのでは。手直しするとこんな感じでしょうか。
(define (includes? set anObj) (define (loop rest) (if (null? rest) #f (if (equal? (car rest) anObj) #t (loop (cdr rest))))) (loop set))
- あ、ほんとだ...。はじめにサイズ調べた方がいいよなーと思ったんですけどnull?でチェックしてるからいりませんね。m.hana (2006/06/05 16:16:47 PDT)
eq?/eqv?/equal?は目的に応じて使い分けるものなので、includes?内に直接書くのはよろしくないのですが、この段階では特に気にしなくても良いと思います。 また、「リストが特定の要素を含むことを調べる」のであれば、R5RSにmemq/memv/memberという手続きが定義されているので、
(define (includes? set anObj) (member anObj set))
これでもよいでしょう。setに#fが含まれていると不味いですが。2006/06/05 07:33:05 PDT
- 結果が #t ないし #f のどちらかになるように
(define (include? set obj) (and (member obj set) #t))
の方がいいと思います。 (member #f '(1 #f 2)) は (#f 3) のようになるので #f が含まれていても大丈夫です。- memberについて調べてみました。要素が含まれる場合は#tじゃなくてその要素を先頭にしたリストを返すんですね。それから#f以外は真として扱われるんでしたね。それで#tとmemberの戻り値でandを取ると。なるほど。m.hana (2006/06/05 17:59:41 PDT)
- また named-let と and、or をつかって:
(define (include? set obj) (let loop ((rest set)) (and (not (null? rest)) (or (equal? (car rest) obj) (loop (cdr rest))))))
- しばらく悩む...。orは(equal? (car rest) obj)が#tだったらその時点で#tを返すわけですか...。#fなら再帰すると。その結果と(not (null? rest))とandを取るということは、えーっと、おお、なるほど。すごい。m.hana (2006/06/05 17:59:41 PDT)
- あるいは、
(define (include? set obj) (cond ((null? set) #f) ((equal? (car set) obj) #t) (else (include? (cdr set) obj))))
- ああ、これはすっきりしてますね。いろいろあるなあ。m.hana (2006/06/05 17:59:41 PDT)