Collatz 予想 (キミならどう書く 2.0 - ROUND 2 -)

時間ができたので考えてみた。そのまま素直に書いてみた。

#!/usr/bin/env gosh

(define (g n)
  (define (f n step)
    (cond ((= n 1) step)
          ((even? n) (f (/ n 2) (+ step 1)))
          ((odd? n) (f (+ (* n 3) 1) (+ step 1)))))
  (f n 1))

(define (h n)
  (let loop ((i 1)
             (k 1)
             (m 1))
    (if (< n i)
        (values k m)
        (let ((s (g i)))
          (if (<= m s)
              (loop (+ i 1) i s)
              (loop (+ i 1) k m))))))

実行結果。

gosh> (h 100)
97
119

n = 97 のときステップ数 119 で最大。

define-syntax でユニットテスト

結城さんが define-syntax を使った debug マクロ(デバッグプリント - 結城浩のSICP日記 - sicp)を紹介されている。マクロの便利な使い方の好例だと思う。Scheme ではなく、Common Lisp の話になってしまうが、高い評価を受けている Practical Common Lisp という本(オンラインで読める)にもマクロを使った面白い例が 9 章に載っている。その章ではマクロを使ってユニットテストのための簡易ライブラリを作っていくのだが、まだ Lisp のマクロというものがよく分からなかった僕はこの内容にとても感銘を受けた。せっかくなのでこの内容の前半をさらっと簡単に Scheme (Gauche) を使って紹介したいと思う。

本ではまずつぎのようなテスト(関数 + のテスト)を例にしている。

(= (+ 1 2) 3)
(= (+ 1 2 3) 6)
(= (+ -1 -3) -4)

これらすべてが #t ならばテストはパスするので、次のような関数を定義する。

(define (test-+)
  (and (= (+ 1 2) 3)
       (= (+ 1 2 3) 6)
       (= (+ -1 -3) -4)))

もちろん (test-+) の評価結果は #t である。

次に、それぞれのテストを分かりやすく表示するようにする。

(define (test-+)
  (define (boolean->string bool) (if bool "pass" "FAIL"))
  (format #t "[~A] ... ~A~%" (boolean->string (= (+ 1 2) 3)) '(= (+ 1 2) 3))
  (format #t "[~A] ... ~A~%" (boolean->string (= (+ 1 2 3) 6)) '(= (+ 1 2 3) 6))
  (format #t "[~A] ... ~A~%" (boolean->string (= (+ -1 -3) -4)) '(= (+ -1 -3) -4)))

かなり冗長だけど、結果はわかりやすくなる。

gosh> (test-+)
[pass] ... (= (+ 1 2) 3)
[pass] ... (= (+ 1 2 3) 6)
[pass] ... (= (+ -1 -3) -4)
#<undef>

重複する部分を関数にまとめておく。

(define (report-result result form)
  (format #t "[~A] ... ~A~%" (if result "pass" "FAIL") form))

(define (test-+)
  (report-result (= (+ 1 2) 3) '(= (+ 1 2) 3))
  (report-result (= (+ 1 2 3) 6) '(= (+ 1 2 3) 6))
  (report-result (= (+ -1 -3) -4) '(= (+ -1 -3) -4)))

やっている内容は同じなので、(test-+) の出力は先ほどと変わらない。しかし、やはりテストしたい式と、それを出力するときにつかう quote された式の二つを書かなければならないので何かと厄介だ。

ここでマクロの出番。check マクロを導入して、テストしたい式が評価される前に式そのものを quote できるようにする。

(define-syntax check
  (syntax-rules ()
    ((_ form)
     (report-result form 'form))))

(define (test-+)
  (check (= (+ 1 2) 3))
  (check (= (+ 1 2 3) 6))
  (check (= (+ -1 -3) -4)))

かなり見通しが良くなった。

gosh> (test-+)
[pass] ... (= (+ 1 2) 3)
[pass] ... (= (+ 1 2 3) 6)
[pass] ... (= (+ -1 -3) -4)
#<undef>

動作も問題ないようだ。

もう少し check マクロを工夫して、複数の式を同時に扱えるようにする。

(define-syntax check
  (syntax-rules ()
    ((_ form)
     (report-result form 'form))
    ((_ form1 form2 ...)
     (begin
       (report-result form1 'form1)
       (check form2 ...)))))

(define (test-+)
  (check
   (= (+ 1 2) 3)
   (= (+ 1 2 3) 6)
   (= (+ -1 -3) -4)))

最初のバージョンに比べるととても簡潔になった。まさにマクロならではといえる。

と、以上で紹介は終わり。しかし、さきの Practical Common Lisp ではマクロをどのように使うかをこの先もステップを踏んで丁寧に説明していく。語り口もうまいので引き込まれてしまう。Common Lisp のマクロシステムは define-syntax のそれとはかなり異なるが、それでもこの本から学べることは多いはず。Scheme な方にもぜひ読んでもらいたいと思い、簡単ではあるけど、今回ブログで紹介してみた。

追記: shiro さんよりコメントを頂きました。最後の check マクロはもっと単純に書けます。

研究者が Python を使うべき5つの理由と避けるべき5つの理由

最近よく見かける10の理由が面白いので書いてみたいと思った。「Pythonを使うべき10の理由」的な内容にしようかと思ったのだが、一般論として書くよりは、個人的に Python を最もよく利用するシーンである、研究室における利用を想定して書いてみた方がより Python の特長の一面を表せるのではないかと考えた。しかし、単に Python マンセーしても仕方ないので、10の理由のうち半分を推奨する理由として、残り半分を推奨しない理由として列挙してみる。

タイトルにある「研究者」というのは、かなり広い意味を持ってしまうので、まずは前提条件として自分の立場を書いておき、それに近い領域の研究者としてみたい。まず僕は「研究者」といっても学生である。また、工学系の分野の研究をしており、シミュレーションよりは実際に手を使った実験が中心である。したがって、大規模科学計算のような分野についてあまり知らない。研究において Python を使うのは、実験で得られた値と理論との一致を確かめるための小規模なシミュレーションや、それを使った未知の物性値の導出、または実験データの整理などがメインである。そういった立場で以下の理由を挙げていきたい。

<研究者が Python を使うべき5つの理由>

1. Lightweight Language である

研究とは、未知のテーマに対して試行錯誤しつつ様々な試みを行っていくプロセスの連続であるから、それに付随するツールも日々変化しなければならない。書き捨てプログラムも多いし、その場その場で柔軟にコードに修正を入れることも多い。だから、Lightweight であることは重要である。

2. オブジェクト指向設計を強要しない

Pythonオブジェクト指向言語として利用できるが、無理してオブジェクト指向設計を行う必要はない。もともと関数で表される理論を無理にオブジェクト指向的に変換する必要はない。そもそも、この分野にいる大半の人間はコンピュータサイエンスとはほぼ無縁に近いから、オブジェクト指向なんてものは知らないし知ろうともしない。ただし、モデルによってはオブジェクト指向設計がうまくハマるから、「強要しない」ことと同じくらいに「できる」ことも重要である。

3. NumPy, SciPy がある

これは重要。非常に重要。もし無かったら僕は Python を研究には使っていない。これらは Python の処理速度を補う上で必須のツールであるし、信頼性の高い計算用ライブラリも利用できる。科学計算用のプログラミング環境は他にも多々あるが、Python という汎用的なプログラミング環境に、NumPy や SciPy によって科学計算の分野が取り込まれることで生まれるメリットは非常に大きい。

4. Windows でインストールが楽

僕のマシンは Mac だが、工学系の研究室においては Windows が主流である。したがって Windows で動く環境でなければならない。最近は UNIX 系のツールも Windows に対応するようになったが、Python の場合は普通のインストーラが使える。ダブルクリックでインストールできる。とりあえず試してみようという気になりやすい。

5. Battery Included かつ豊富なサードパーティライブラリ

研究でプログラムするのは、先にも述べたとおりシミュレーションや実験データの整理などが目的だ。しかしそれら意外の処理ももちろん必要となる。ファイルの入出力のようなシンプルなものから、人によってはネットワークを使ったデータのやり取りや、データベースの利用、場合によっては分散処理も必要になるかもしれない。こういった場面においても Python は様々にサポートしてくれる。

<研究者が Python を避けるべき5つの理由>

1. C ではない

コンピュータサイエンスとは基本的には無縁だが、やはりプログラミングが必要な分野では「とりあえず C やっとけ」みたいな雰囲気がある。で、みんな嫌々ながらも C をやる。しかし当然興味もないのですぐ忘れる。しかしその割に、「プログラミングといったら C でしょ」みたいなイメージは持っている。というか、その他のプログラミング言語を知らない場合もある。そんな人に、「Python どうですか」って突然言っても「へ?」である。

2. FORTRAN でもない

FORTRAN の呪縛はものすごい。いまだに科学計算の分野ではバリバリの現役の上、FORTRAN の持つ資産もものすごい。この分野における COBOL。幸い、Python には f2py があるし、NumPy や SciPy のように FORTRAN と優しく適度な距離感をもって接することは不可能ではない。

3. インタープリタである

ある程度プログラミングを知っている人の場合、次に来るのは「インタープリタだろ」というものだ。たしかに今のマシンは Python で Web アプリケーションを動作させるのに十分な速度を持ったかも知れないが、数値計算の要求はオーダーが数桁異なるので素の Python では力不足感は否めない。でも NumPy や SciPy を利用すれば大抵は問題にならない。

ちなみにパフォーマンスに関する問題は 80-20 の法則とからめて、必要ならそこだけ拡張ライブラリとして C で書けば良い、という話があがる。たしかにそれはそうだが、拡張ライブラリを書く人はあまりいないので、既に 80-20 のボトルネックとなる部分が解決できるライブラリが存在していることは重要だ。

4. まわりの誰も知れない

プログラミングを好きでするというよりは、必要だからする人にとって、誰も知らないのはハードルが高い。こればかりは頑張って Python を広めるしかない。ただ、一つ言っておきたいことは、たしかに C はみんな知っているが、知っているのは名前だけであって、使い方を知っている人はあまりいないということだ。だったらさっさと独学で Python を始めたほうが目的地へは早くつくと思う。

5. 生協に本が売っていない

C も JavaMATLAB も置いてある。Perl も、Ruby も置いてある。でも Python ないよというケースは多々あるんではないか。悔しい。ただし「初めてのPython 第2版」は発売当時置いてあった。生協の中の人、GJ。

おわり。

for

Scheme だとあまり使う場面はないだろうけど、普通の for 文が使いたかったので書いた。一応残しておく。

(define-syntax for
  (syntax-rules ()
    ((_ (var start end) body ...)
     (do ((var start (+ 1 var)))
         ((= var end))
       body ...))))

実行例。

gosh> (for (i 0 10) (format #t "~d " i))
0 1 2 3 4 5 6 7 8 9 10 #t

Emacs できれいにインデントしたい場合は、.eamcs などで

(put 'for 'scheme-indent-function 1)

しておく。

Python のメソッドをクロージャとして使う

Python においてメソッドはユニークな性質を持っており、bound されているか unbound か、明確な違いがある。bound / unbound とは、そのメソッドが特定のインスタンスに属しているか、いないかという言い方が出来ると思う。

class Person(object):
    def __init__(self, name, age):
        self.__name = name
        self.__age = age

    def get_name(self):
        return self.__name

    def get_age(self):
        return self.__age

単純なクラスを定義してみた。このクラスのメソッドを調べてみると、

>>> Person.get_age
<unbound method Person.get_age>

こちらは unbound となっているが、

>>> p = Person("A", 10)
>>> p.get_age
<bound method Person.get_age of <test.Person object at 0x56eb0>>

インスタンスの場合は bound となっている。

unbound メソッドを使うためには、"self" に相当するインスタンスを与える必要がある。

>>> f = Person.get_age
>>> f()
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
TypeError: unbound method get_age() must be called with Person instance as first argument (got nothing instead)
>>> p = Person("A", 10)
>>> f(p)
10

一方で、bound メソッドは関数として使うことができる。

>>> p = Person("B", 20)
>>> g = p.get_age
>>> g()
20

上記の関数 g() は、実態はbound メソッドだが、外部から見た場合は単なる関数だ。しかし g() の内部では p.__age を参照することができるから、クロージャとして見ることができる。

さて、複数の Person のインスタンスの age の平均値を求める関数があるとする。

def average_age(persons):
    return sum([p.get_age() for p in persons]) / len(persons)

この関数をもう少し抽象化して、とにかく関数リストが与えられたらそれらの返り値の平均を求めるようにしてみる。

def average_func(funcs):
    return sum([f() for f in funcs]) / len(funcs)

Person の get_age() を bound メソッドとしてリストにして使えば良い。

>>> persons = [Person("A", 10), Person("B", 20), Person("C", 50)]
>>> funcs = [p.get_age for p in persons]
>>> average_func(funcs)
26

average_func() 自体は Person とは関係のない汎用的な関数であり、色々な場面で使えるはず。こうしたクロージャ高階関数を使ったパターンは Scheme ではよく目にするのだが、Python でも同様の概念を実現することが出来る。しかもクラス、メソッドなどの Pythonオブジェクト指向言語としての要素をそのまま組み合わせれば良いので、全体の中に自然にとけ込ませることが出来るのではないだろうか。やはり Python は良くできた言語だ。

もっとも今回のようなケースはわざわざ average_age() を導入するような場面ではなかったと思う。もう少し面白い例があればよかったのだが、思い浮かばず。

Python の関数型っぽいプログラミング

Python はかなり関数型っぽいということがやっと分かったので、Scheme と比べつつ様子を探ってみたい。まずは Lisp ではよく高階関数の例として取り上げられるものをいくつか取り上げてみた。

関数の合成。

;; Scheme
(define (compose f g)
  (lambda (x)
    (f (g x))))

# Python
def compose(f, g):
    def __fg(x):
        return f(g(x))
    return __fg


つづいて二つのリスト(あるいは配列)の各要素ごとの和を返す関数。

;; Scheme
(define (sum-element lst1 lst2)
  (map + lst1 lst2))

# Python
def sum_element(lst1, lst2):
    return map(operator.add, lst1, lst2)

実際に使う場合は、どちらも関数として定義することはないだろうけど。Python で map() と operator.add がなかったらかなり冗長になってしまうと思う。zip() とリスト内包表記を使えばまだ良いかもしれない。

最後にリストの積を返す関数。Scheme

;; Scheme
(define (accumulate-list lst)
  (fold + 0 lst))

# Python
def accumulate_list(lst):
    return reduce(operator.add, lst, 0)

# Python (手続き的処理、高階関数なし)
def _accumulate_list(lst):
    acc = 0
    for x in lst:
        acc += x
    return acc


Python を学んでいた当初は、高階関数という概念はよく知らなかったので、上記のような書き方はせずに、ループ構文を使ったり、リスト内包表記などで書いていたのだが、高階関数に慣れれば場合によっては上記のようなコードの方がずっと分かりやすい。Guido は map や reduce などは嫌いっぽいが、やっぱり便利なんで残して欲しい。あと operator モジュール重要。

NumPy, SciPy

久しぶりにちゃんと Python でプログラム。ここ最近、Python 周辺も色々な変化があった。最近注目されている TurboGearsDjango のような Web アプリケーション関係は興味はあるものの、それよりまずは NumPySciPy

ちょっと前まで Python数値計算といえば、Numeric か numarray だったが、今後は NumPy へ統一していこうということらしい。Numeric との互換性は念頭において開発されているので、今までの知識も活かせるし、プログラムの移植もすぐ出来そう。

そこで、自分のマシンに NumPy と SciPy を DarwinPorts でインストールしてみた。ところが、SciPy のコンパイルがコケる。Fortrun コンパイラのあたりでダメになる。あんまり時間かけてらんないので、とりあえず Portfile をいじって gcc 3.4 の g77 を使うように変更してインストール。

ところで、ここ半年近く Scheme を中心とした関数型言語を満喫していたわけだけど、今度は Python が非常に新鮮に見えた。Python ってすごく関数型っぽい。他の言語を経由すると、既知の言語に対する視点も随分変わる。面白いなぁ。