よりClojureらしい素数列
私は一昨日から2011-09-24 - 蟲!虫!蟲! - #!/usr/bin/bugrammerの補足を書いていましたが、分量が多すぎるので後日分割することにしました。
そこで、代わりに2011-09-27 - 蟲!虫!蟲! - #!/usr/bin/bugrammerの問題を解いて解説をつけたものを先に公開します。
しかし、それも解説を含めると長いので、問題ごとに分割して記事を公開します。ツッコミ歓迎。
始める前に
解説してない部分はdocを使って調べて、疑問が残っていたら気軽に質問してください。
まずdocstringから始めよ
まず目につくのは名前と引数の間にある文字列ですが、これはdocstring(ドキュメンテーション文字列)と呼ばれます。Javadocとほぼ同じ役割を持ち、docなどで見られる説明はこれです。
docstringを書くのは(関数名と引数を繰り返すようなものを書かなければ)良い習慣です。もちろん、内容を変更したらdocstringも更新します。
素数?
Clojureでは、真偽値*1を返す(副作用のない)関数を述語と呼び、述語には?で終わる名前を付ける慣習*2があります。
ここに登場する述語はzero?、not-any?、prime?です。zero?は名前の通り、数値を受け取って0ならtrueを、そうでなければfalseを返す述語です。
内容を読み解かなくても、prime?が述語だということは想像できます。not-any?という(正体をまだ知らない)述語の返り値をそのまま返しているからです。
not-any?は述語とシーケンス*3を受け取り、シーケンスの要素一つ一つに対して述語を適用して、trueを返すものが一つもなければtrue、そうでなければfalseを返す述語です。
(not-any? zero? '(1 2 3)) ;=> true (not-any? zero? '(0 1 2)) ;=> false
ここで;=>と書いたのは、左側の式を評価*4すると右側の値が返ってくるという意味です。
つまり、
(not-any? zero? ,,,)
は,,,に来るシーケンスの要素に0がない場合にtrue、ある場合にfalseを返します。
そして,,,の部分はこうなっています。
(map #(rem x %) (range 2 (inc (/ x 2))))
これは、2以上(x/2)+1未満を満たす整数のシーケンスを作り、それぞれの要素でxを割った余りのシーケンスに変換しています。
x/2が現れるのは、最小の素数が2なので、どんな合成数x自分自身を除くx/2より大きい自然数で割り切れることはないからです。*5
#(rem x %)は(partial rem x)とも書けますが、ここでは深入りしません。関数に慣れた頃に調べると良いと思います。
まとめると、prime?は「xが2以上(x/2)+1未満のどの整数でも割り切れなければtrue、そうでなければfalseを返す」関数です。
しかし、docstringに書くべきことはアルゴリズムではなく意図や用途ですから、「xが素数であればtrue、そうでなければfalseを返す」と書いています。
素数とは
このコードの山場はprime?なので、後はそう複雑ではありません。
(filter prime? (drop 2 (range)))
rangeは引数がない場合に0, 1, 2, 3, ...を要素に持つ無限シーケンスを返す関数です。
無限シーケンスと言っても、無限のメモリを要求するわけではありません。rangeを始め、多くのシーケンス関数は必要になるまで計算しません*6。
(set! *print-length* 10) ;=> 10 (range) ;=> (0 1 2 3 4 5 6 7 8 9 ...)
(set! *print-length* 10)が重要な所で、これはREPLなどがシーケンスを表示しようとする際に先頭から何個表示するかを指定します。これがないと、無限シーケンスの全てを表示しようとして、スレッドやプロセスを止めるまで反応しません。
(drop 2 (range))
dropは整数とシーケンスを受け取り、先頭からn個捨てて残りを返す関数です。ここでは0と1を捨てます。
(filter prime? (drop 2 (range)))
filterは述語とシーケンスを受け取り、述語に適用してtrueを返す要素だけを持つシーケンスを返す関数です。
まとめると、prime-numbersは「2, 3, 4, ...の中の素数を抜き取った無限シーケンスを返す」関数ですが、要するに素数列を返す関数です。
欲しい時に、欲しい分だけ
元のコードでは整数を一つ取って「xより小さい」素数をシーケンスとして返すようになっていましたが、これは引数がありません。しかし、「xより小さい素数」が必要ならすぐ手に入れられます。試しに100未満の素数を手に入れます。
(take-while #(< % 100) (prime-numbers))
先頭から100個欲しい時もあるかもしれません。
(take 100 (prime-numbers))
100番目の素数が欲しい時もあるかもしれません。
(nth (prime-numbers) 99) ; Clojureでは0番目から数え始める点に注意!
prime-numbersは遅延シーケンスによって計算を必要になるまで延期しますが、同時に「何が必要なのか」という選択も必要になるまで延期できるようになりました。
今、いくつかの関数を解説せずに使いましたが、初めて見たものはdocで調べてください。例えば、こんな風に。
(doc take-while)
おわりに
このコードは効率より読みやすさを優先しました。速度を求める場合はより洗練されたアルゴリズムを使う必要があるでしょう。
より高速なバージョンの一つについて、第五問の解説の中で言及します。今回書かない理由は解説の流れの都合上そうした方が良いと判断したからです。ごめんなさい。
第三問については特に補足するような事もないので、第二、四、五問の添削もいずれ行いたいと思っています。*7
私より優れたClojurian*8はたくさんいるので、誰かが私のコードを更に添削するかもしれません。むしろ添削してください。お願いします。
追記:Gaucheで「よりClojureらしい素数列」
Gaucheの作者、川合史朗さんがこのページのコードをGaucheで書き直したものをhttp://blog.practical-scheme.net/shiro/20110927-primesで公開しています。