Gaucheプログラミング(立読み版) > 第1部: 思想 > 「Lisp脳」の謎に迫る - Schemeプログラマの発想


[Prev] [Next] [Up] [Contents][フレーム表示] [フレーム解除

「Lisp脳」の謎に迫る - Schemeプログラマの発想 応援する 

この原稿の最新版について

この原稿に加筆した最新版が書籍「プログラミングGauche」に収録されています。 引用や紹介をされる方はなるべく書籍収録版を参照してください。

他の言語のプログラマがSchemeプログラムを書くとき、 どうしても発想が手続き的(procedural)になりがちです。

LispプログラマやSchemeプログラマの発想は手続き的な発想とはどうも違うらしい、 ということは分かるのですが、具体的に何が違うのでしょうか?

ここではこの謎に迫ってみましょう。

実例

例えばこんな例題があります。

1から100までの数をプリントするプログラムを書け。ただし3の倍数のときは数の代わりに「Fizz」と、5の倍数のときは「Buzz」とプリントし、3と5両方の倍数の場合には「FizzBuzz」とプリントすること。

どうしてプログラマに・・・プログラムが書けないのか?
(原題: Why Can't Programmers.. Program?)
Jeff Atwood / 青木靖 訳
http://www.aoky.net/articles/jeff_atwood/why_cant_programmers_program.htm

「プリントすること」と書かれているので、 手続き的プログラマこんな手順を考えるのではないでしょうか。

  1. 1から100までの数を繰り返し処理で処理する
  2. 繰り返し毎にその回の数を判定して条件分岐する
  3. 15で割りれるなら"FizzBuzz"を、5で割り切れるなら"Buzz"を、3で割り切れるなら"Fizz"を、さもなければ数のまま印字する

これを考えていくと、

  • 「プリントってことは入出力を伴うよなあ」
  • 「print手続きを毎回呼ぶんだよなあ」
  • 「繰り返し処理ならforだけどSchemeにはないからdoかなあ」
  • 「回数ごとに1づつ加算するにはGaucheだとinc!が使えるらしい」
  • 「Schemeでは条件分岐にはcondが使えるのか」
  • 「...」

などと連想が広がっていくことでしょう。

この発想を素直に書いたのが次のコードです。

(do ((x 1 (inc! x)))
    ((>= x 100) x)
  (cond ((= (modulo x 15) 0)
         (print "FizzBuzz"))
        ((= (modulo x 5) 0)
         (print "Buzz"))
        ((= (modulo x 3) 0)
         (print "Fizz"))
        (else
         (print (x->string x)))))

実行してみると1から100までの数を印字改行し、 3や5の倍数には仕様どおりに"Fizz" "Buzz" "FizzBuzz"が印字改行されています。 めでたしめでたし。

Schemeプロラマがこんな風に考えないのだとしたら、 いったい何が違うのでしょうか?

Schemeプログラマの発想

Schemeプログラマはたとえばこんな風に発想します。

「とりあえず1から100までのリストを作れば良さそうだ」

と考えて、まずこんな風に書いてみます。

 gosh> (use srfi-1)
 #<undef>
 gosh> (iota 100 1)
 (1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80
 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100)

つぎに、

「mapを使ってこのリストを加工して、別のリストを返せばいいじゃん」

と考えます。

 (map
  何らかの加工
  (iota 100 1))

map式では元のリストの要素すべてに「何らかの加工」を行うと新たなリストが返って来ます。 あとはこの「何らかの加工」の中身を考えれば良さそうです。

「何らかの加工」の中身ですが、 Schemeプログラマは「プリントせよ」という文を読んだからといって素直に「印字せよ」とは考えません。 (このへん若干誇張されている恐れあり)

「その要素を置き換えておけば、出力は後からどうにでもなるじゃないか」

と考えて、

  (cond ((= (modulo x 15) 0)
         (print "FizzBuzz"))
        ((= (modulo x 5) 0)
         (print "Buzz"))
        ((= (modulo x 3) 0)
         (print "Fizz"))
        (else
         (print (x->string x))))

とprint式を書く代わりに、

  (cond ((= (modulo x 15) 0)
         "FizzBuzz")
        ((= (modulo x 5) 0)
         "Buzz")
        ((= (modulo x 3) 0)
         "Fizz")
        (else
         x))

と値を返すだけで満足してしまいます。

 (map
  (lambda (x) (cond ((= (modulo x 15) 0) "FizzBuzz")
                    ((= (modulo x 5) 0) "Buzz")
                    ((= (modulo x 3) 0) "Fizz")
                    (else x)))
  (iota 100 1))

実行してみると確かに問題の仕様どおりのリストが返るので、

「プリントせよ? 対話型インタプリタで実行すれば結果のリストはインタプリタが印字してくれるじゃないか」

とも考えるのですが、

「まあ結果のリストをprintしてみても良いか」

とも考えて全体をprint式で包んでみたりもするかも知れません。

 gosh> (print
        (map
         (lambda (x) (cond ((= (modulo x 15) 0) "FizzBuzz")
                          ((= (modulo x 5) 0) "Buzz")
                          ((= (modulo x 3) 0) "Fizz")
                          (else x)))
         (iota 100 1)))
 (1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz 16 17 Fizz 19
 Buzz Fizz 22 23 Fizz Buzz 26 Fizz 28 29 FizzBuzz 31 32 Fizz 34 Buzz Fizz
 37 38 Fizz Buzz 41 Fizz 43 44 FizzBuzz 46 47 Fizz 49 Buzz Fizz 52 53 Fizz
 Buzz 56 Fizz 58 59 FizzBuzz 61 62 Fizz 64 Buzz Fizz 67 68 Fizz Buzz 71
 Fizz 73 74 FizzBuzz 76 77 Fizz 79 Buzz Fizz 82 83 Fizz Buzz 86 Fizz 88 89
 FizzBuzz 91 92 Fizz 94 Buzz Fizz 97 98 Fizz Buzz)
 #<undef>

もちろん要素ごとに印字して改行したければ、

(for-each print
          (map
           (lambda (x) (cond ((= (modulo x 15) 0) "FizzBuzz")
                             ((= (modulo x 5) 0) "Buzz")
                             ((= (modulo x 3) 0) "Fizz")
                             (else x)))
           (iota 100 1)))

for-eachを使ってリストの要素それぞれに対してprintを適用しても良いでしょう。

結局何が違うのか?

手続き的な発想では、毎回特殊な処理を行いそれを繰り返すという発想でプログラミングしていました。

Schemeプログラマはそうは考えません。

データからデータへの変換を考えれば良く、出力は後からどうにでもなる、 と考えています。

別の側面から見てみましょう。

 (map
  何らかの加工
  (iota 100 1))

データからデータへの変換を行っている「何らかの加工」の部分は、 例えば問題の条件が増えたとしても、いくらでも差し替えが可能です。

これを、「Schemeプログラマの発想はモジュール性が高い」と理解しても良いのですが、 Schemeプログラマは最初から「モジュール性が高くなるように設計しよう」と意図して発想しているのではないのです。

むしろ「データからデータへの変換をしておけばいいじゃん」とだけ考えていたら結果としてモジュール性が高くなってしまったと理解するべきです。

Schemeプログラマの発想の一端がお分かりいただけたでしょうか?


[Prev] [Next] [Up] [Contents][フレーム表示] [フレーム解除

このサイトについて|ヘルプ|Q&A|個人情報保護|プライバシーポリシー|利用規約|コメント・トラックバック規約|削除規程|広告掲載
Copyright (c) 2005-2007 Time Intermedia Corporation