よりClojureらしく線形合同法を使った乱数列を計算する

Clojure入門がてらに「初心者用課題」を解いてみる - 蟲!虫!蟲! - #!/usr/bin/bugrammerの第四問を解いて、解説を加えたものです。

前回(よりClojureらしく閏年を探す - OGINO Masanori@はてな)を踏まえた説明なので、前回を読んでいない方はそちらを先に読んでください。

漸化式を使ったシーケンス

はてな線形合同法のページは、線形合同法を次の漸化式で表現しています。*1

x_n=\left\{\begin{array}{lcr}s&(n&=& 0)\\(ax_{n-1} + b) \bmod m&(n&\geq& 1)\end{array}\right.

この式のs, a, b, mは最初に値を決めたら変化しません。今回はlcgの引数として受け取ることにします。ちなみにsは乱数の種(seed)です。

今回の問題では、x1, x2, x3, ...を要素として持つシーケンスがあればいいので、漸化式を変形する必要はありません。

繰り返し、繰り返し

前の値を二つ目の式に繰り返し与えながらシーケンスの要素にすればいいのですが、Clojureにはそのための関数があります。iterateです。

iterateは関数と初期値を受け取り、x, (f x), (f (f x)), ...を要素に持つ無限シーケンスを返す関数です。

;; REPLに入力する前に*print-length*にあまり大きくない整数をset!すること!
(iterate inc 0) ;=> (0 1 2 3 ...)

iterateに渡す関数は、今回も第一問のように#()で作ってもよかったのですが、せっかくなのでfnを使ってxと書けるようにしました。fnは関数を返します。

「fnは関数を返す関数」?

前々から気付いていたかもしれませんが、私はよく「○○は△△を受け取り、××を返す関数です」という形で関数を紹介してきました。

しかし、fnの説明は「返します」で終わっています。それはfnが関数ではないからです。fnは特殊フォーム(special form)で、特殊フォームは関数ではありません。

特殊フォームとは何なのか、特殊ではないフォームはどこにあるのか、そもそもフォームとは何を指すのか。それらについてはいずれ別の文章の中で書く予定です。

remか、modか

Clojureはいわゆる「余り」を返す関数を2つ持っています。今まで使ってきたremと、もう一つはmodです。

今回はx, a, b, mのどれかに負の数が入らなければremを使ってもmodを使っても値は同じですが、式にmodとあるのでmodを使います。*2

remとmodの返り値が異なる例を示します。

;; 17 = -5 * (-3) + 2
(rem 17 -5) ;=> 2
;; 17 = -5 * (-4) + (-3)
(mod 17 -5) ;=> -3

これで、乱数のシーケンスを手に入れました。あとはdropでx0を捨てて*3、元のコードに合わせるために逆数を取ってから浮動小数点数に変換します。

/は引数が一つの場合に数値を一つ受け取り、その逆数を返す関数です。floatは数値を一つ受け取り、(単精度二進)浮動小数点数に変換して返す関数です。

(/ 0.5) ;=> 2.0
(/ 2) ;=> 1/2
(float 1/2) ;=> 0.5

見ての通り、Clojure有理数型と有理数リテラルを持ちます。

有理数型のない言語に慣れている方は落ち着かない気分になるかもしれませんが、必要になるまで浮動小数点数に変換しないようにすると丸め誤差の影響を軽減できます。

平均を求める

元のコードだとrandom-meanと書かれていますが、このmeanは平均を意味している*4と思うのでlcg-averageという名前にしました。

また、元のコードではシーケンスと平均を出力していますが、好みで*5平均を返すようにしています。シーケンスも必要ならリストか何かに一緒に入れて返せば実現できます*6

このシリーズで初めて登場するletとapplyを以下で解説します。

名前を付ける

letは値に名前を付けることができます。*7値に名前を付ける(または名前と値を結び付ける)ことを「代入」と呼ぶプログラミング言語もありますが、Clojureは「束縛」と呼ぶプログラミング言語の一つです。

(let [x 1]
  ,,,)

この例で言うと、「letはxを1に束縛する*8」または「xは(letによって)1に束縛される」と言います。

束縛したい名前と対応する値は[名前 値 名前 値 ...]と交互に並べます。*9*10束縛された名前は、letの内側で参照できます。そして、letも式であり、内側の式の値がlet全体の値になります。

(let [x 2
      y 3]
  (+ x y)) ;=> 5

letについて書くことはまだあるのですが、今回はlet以外にも説明すべきことがあるのでここで止めておきます。

適用する

applyは関数と「その関数の引数にしたいもの」を受け取り、その関数に「引数にしたいもの」を渡して呼び出し、その関数の返り値を返す関数です。

「引数にしたいもの」を持っていながら、その関数を直接呼び出せないことがあります。それは引数をリストなどの形で持っている時です。applyはそうした時に使います。

applyを「引数として渡したいリストを一つ受け取る関数」と勘違いする*11ことは珍しくありませんが、Clojureのapplyはリストなどの前に引数をそのまま書けます。

(apply + 1 2 3 '(4 5)) ;=> 15

だからといって、次のようなことをしても意味はありません。*12

(apply + 1 2 3 4 5 '()) ;=> 15

そして、説明が遅れましたが、+は3個以上の引数を受け取ることができます。

(+ 1 2 3 4 5) ;=> 15

これでlcg-averageの解説も十分だと思います。

復習問題

余裕があればよりClojureらしい素数列 - OGINO Masanori@はてなよりClojureらしく閏年を探す - OGINO Masanori@はてなの(drop ,,, (range))を(iterate ,,, ,,,)の形に書き直してみましょう。

Clojureのコードも「やり方は一つじゃない」のです。

おわりに

今回は今までより文章の量が多いので、読むのがつらかったかもしれません。ごめんなさい。

次回は「数当てゲーム」を予定しています。今回説明するまでiterateを使わなかったように、これまでの三問で使わなかったものを使ったプログラムになります。

*1:意味を変えない範囲で変形しました。

*2:線形合同法で使う剰余の定義を確かめるのが「正しい」方法です。

*3:ここはrestでも構いませんが、今までの解説から「1個捨てる」ことが分かりやすいようにdropを使いました。restを使った方が読みやすい場合もあります。

*4:違ったら指摘してください。

*5:「xが大きい場合はシーケンスの出力に時間がかかる」という理由もありますが、「出力するよりも、単に返して呼び出す側が出力するかどうか選択する方が好き」という理由が大きいです。

*6:多値のある言語に慣れた方へ:Clojureに多値そのものはありません。

*7:letも特殊フォームです。

*8:「代入」するプログラミング言語では「xに1を代入する」と呼ぶことに気をつけてください。「に」と「を」が入れ替わっています。

*9:Clojureには分配束縛という機能もありますが、ここでは解説しません。

*10:他のLispに慣れている方へ:Clojureではnilで初期化したい場合に(let [foo nil] ,,,)のようにnilを明示します。

*11:そういうapplyを実装した言語もあります。

*12:Clojureは変数と関数の名前空間が分かれていませんから、たとえ変数の値としての関数であっても普通に呼び出せます。