2007年05月18日

Lisp脳

「Lisp脳」の謎に迫る - Schemeプログラマの発想
他の言語のプログラマがSchemeプログラムを書くとき、どうしても発想が手続き的(procedural)になりがちです。
LispプログラマやSchemeプログラマの発想は手続き的な発想とはどうも違うらしい、ということは分かるのですが、具体的に何が違うのでしょうか?

このリンク先のページは、以下のプログラムを実装する場合の手続き型言語使用者と関数型言語使用者の考え方の違いを解説してる。
1から100までの数をプリントするプログラムを書け。ただし3の倍数のときは数の代わりに「Fizz」と、5の倍数のときは「Buzz」とプリントし、3と5両方の倍数の場合には「FizzBuzz」とプリントすること。

Lisp脳だと1~100のリストに対してmapすると言うのは確かにそうかも。と言うかSICPではそういう考え方のプログラムが多いようだ。
Lisp脳になれきれていない俺の場合だと、1スタートで100までテイルリカージョンしつつ条件に一致する値の場合だとそれぞれの文字列を返すって感じで。(全然テイルリカージョンじゃなかったので本当のテイルリカージョンバージョンはコメント欄に)

(define (FizzBuzz start end)
(cond ((>= start end) '())
((= (remainder start 15) 0) (append '("FizzBuzz") (FizzBuzz (+ start 1) end)))
((= (remainder start 5) 0) (append '("Buzz") (FizzBuzz (+ start 1) end)))
((= (remainder start 3) 0) (append '("Fizz") (FizzBuzz (+ start 1) end)))
(else (append (list start) (FizzBuzz (+ start 1) end)))))


って感じかな。再帰だけどループを前提に考えてるからまだまだLisp脳には遠いようだ。
あ、'("FizzBuzz")と(list "FizzBuzz")って同じだったっけ?まぁいいや。
再帰でFizzBuzz関数を呼び出す部分が4回も冗長して書かれているので、やっぱりmapを使う方がスマートかな。

このページのまとめで「"データからデータへの変換をしておけばいいじゃん"とだけ考えていたら結果としてモジュール性が高くなってしまった」と言うのは確かにそうだなぁ。考え方の違いでよりよいプログラムになればそれはそれで大歓迎だ。

たとえばJavaもJUnitで自動化テストをしようとMockオブジェクトを使えるようにしたら、結果抽象度が上がり他のクラスとの依存度が下がって柔軟なクラスになると。

Lisp脳に達するにはまだまだかかりそうだけど、その前にJava脳になりきることを先に考える必要があるな。
まだまだ実装クラスと実装クラスが依存してしまうように書いてしまう。interfaceに依存するように書かなくちゃいけないけどやっぱめんどくさいw
そもそも「ここは実装クラスに依存するコードで良いか悪いか」をちゃんと考えなくちゃなぁ。今の現場はJavaプログラマが俺だけで、誰も俺のコードを見ない環境なのでついつい「めんどくさいからこんなんでイイや」ってなってしまう。もっと自分に厳しく行かねば!

Comment on "Lisp脳"

面白い記事の紹介ありがとう。
むーん、しかし上のコードは tail recursion になってないよ。

問1.上のコードを tail recursion (iterative process) に直せ。
問2.上のコードをみた Eva Lu Ator は「これは append を用いずに書けるよ。」といった。append なしで書くとすればどうするか。

  •   (lambda (x) (
  • 2007年05月18日 14:13

ぎゃ、しまった。全然tail recursionじゃなかったw
と言うことで今度こそ。そしてcons使ってみました。
cons使うとループの考え方が普通と逆になるからついappend使ってしまうんだよなぁ。
(define (FizzBuzz start end result)
 (define (getItem x)
  (cond ((= (remainder x 15) 0) "FizzBuzz")
      ((= (remainder x 5) 0) "Buzz")
      ((= (remainder x 3) 0) "Fizz")
      (else x)))
 (if (> start end)
   result
   (FizzBuzz start (- end 1) (cons (getItem end) result ))))
(FizzBuzz 1 100 '())
再帰の呼び出しも1個になりスマートになった!
でもやっぱり"手続き的(procedural)"な考えだな。
LISt Processing的にはリストにmapするのがベストなんだろう。

  •   けんじ
  • 2007年05月18日 15:13

正解!

なお問2は単に、
(append '("hoge") list)
=> (cons "hoge" list)
と書き直せるということを言いたかった。

cons で recursive を iterative に書き直すとたしかにループの順番が逆になってわかりづらくなるね。
僕も混乱する。めんどくさいときは最後に一回だけ reverse するというのをよくやります。

(define (FizzBuzz2 start end result)
(define (getItem x)
...略
)
(if (> start end)
(reverse result)
(FizzBuzz2 (1+ start) end (cons (getItem start) result))))

  •   (lambda (x) (
  • 2007年05月18日 16:13

ふ~む、reverseするのも手なんですね。
そっちの方がわかりやすいかも。
今度からそうしよう。

ちなみにいろんな言語でFizzBuzzを短く書くコンテスト
http://golf.shinh.org/p.rb?FizzBuzz#Scheme
Schemeは79文字が最短のよう。
僕のは236文字でした。79文字ってどんなコードだろう・・・。
日本人の名前が結構ありますね。

  •   けんじ
  • 2007年05月18日 19:09

がんばったけど、147 bytes だなぁ。
(define(x i)(if(> i 100)'()(cons i(x(1+ i)))))(map(lambda(n)(cond((=(gcd n 15)15)"FizzBuzz")((=(gcd n 3)3)"Fizz")((=(gcd n 5)5)"Buzz")(t n)))(x 1))

これには gcd が3回出てくるので、くくりだしたら逆に1バイト増えて148になるw。
(define(x i)(if(> i 100)'()(cons i(x(1+ i)))))(map(lambda(n)(define(y j)(=(gcd n j)j))(cond((y 15)"FizzBuzz")((y 3)"Fizz")((y 5)"Buzz")(t n)))(x 1))

scheme には上のページの Gaucheの iota のような便利な関数すらないので、そもそも 1から100のリストつくるだけで 46文字も使ってる。(上の関数(x i) のこと)

79にするにはたぶん全く違う発想の転換が必要なんだろうね。

  •   (lambda (x) (
  • 2007年05月21日 14:04