Rubyで「Lisp脳」に迫る。

はじめに

 再利用性の高いプログラムを書くにはどうしたらよいのだろう、と、いつも思う。

 学生のころ(もう10年以上前だけど)、 BASIC と C と Verilog を勉強して、社会人になってから Ruby をちゃんと勉強した。正確には学生のころも Ruby さわったことがあったんだけど、「正規表現が使えてセミコロンがいらない C 」くらいにしか思ってなくて、それよりも踏み込んで便利さを知ったのは、けっこう最近。

 再利用性が高いプログラムを書くのに、Ruby はやっぱり便利だ。

 Ruby が便利な理由としては「メタプログラミングが得意」とか「オブジェクト指向だから」とか、いろいろ言われるけれど、個人的には「『DoA(Data Oriented Approach)』を気軽に実践できる」というのが大きいと思う。

  DoA というのはそのまま訳すと「データ中心アプローチ」となって、データ構造の変遷を中心にプログラムを設計していく考え方だ。「PoA(Process Oriented Approach)≒手続き中心アプローチ」に代わって用いられるようになった設計手法で、業務システムとかデータベースを中心に据えたシステム設計でよく用いられる言葉らしい。

DoA 参考資料
データ中心アプローチ
【150号】 POAの世界観とDOAの世界観

表題に現れる「Lisp脳」ってそもそもなんだよ、と思われるかもしれないが、過去にそれについて記載した神記事が存在していたのである。が、いまはデッドリンクになってしまっていた。

ttp://karetta.jp/book-node/gauche-hacks/023107

ここでいうLisp脳は一言で言うと、「つねに『データからデータの変換を意識する』」というもので、私は「DoA≒Lisp脳」なのではないかと思っている。かくいう私自身は Lisper ではないのだが、今回は FizzBuzz 問題を例にここから少し掘り下げて Ruby で Lisp脳 に迫ってみたいと思う。以下、手続き型を PoA 、Lisp脳型を DoA と表現している。それぞれの考え方の違いをご覧いただき、「Lisp 脳」の片鱗を感じていただければ幸いである。

FizzBuzz 問題とその応用における PoA と DoA の違い

というわけで、今回の題材はいわゆるFizzBuzz問題。

1から100までの数をプリントするプログラムを書け。ただし3の倍数のときは数の代わりに「Fizz」と、5の倍数のときは「Buzz」とプリントし、3と5両方の倍数の場合には「FizzBuzz」とプリントすること。
(出典)どうしてプログラマに・・・プログラムが書けないのか?
 (原題: Why Cant Programmers.. Program?) Jeff Atwood / 青木靖 訳
http://www.aoky.net/articles/jeff_atwood/why_cant_programmers_program.htm

 以下、基本形をはじめとして、そこから発展させた応用例を PoA と DoA それぞれで設計し、Ruby で実装した例を示していく。


【基本】「まず基本的な FizzBuzz 問題を実現するプログラムを書きなさい」

 この時点では、どちらも比較的簡単なプログラムで書けるため、PoA と DoA で大きな違いは無いように見える。実際、再利用を想定せずに即席で設計する場合は、PoA のほうが早く書ける場合が多々ある。DoA では、まずはじめに「1から100までの配列(=データ)」を用意し、そのデータ(配列)に「どのような変化を与えればよいか」を考えていく。

# ----------------------------
# PoA (`・ω・キリッ
# ----------------------------
1.upto(100){|i|
 if    i % 15 == 0
   puts "FizzBuzz"
 elsif i % 5  == 0
   puts "Buzz"
 elsif i % 3  == 0
   puts "Fizz"
 else
   puts i
 end
}


# ----------------------------
# DoA (`・ω・キリッ
# ----------------------------
base = Array.new(100){|i| i + 1 }
base.map!{|i| ( i % 15 == 0 ) ? "FizzBuzz" : i }
base.map!{|i| ( i %  5 == 0 ) ? "Buzz" : i }
base.map!{|i| ( i %  3 == 0 ) ? "Fizz" : i }
puts base

【応用1】「同じ処理を 100 まで、200 まで、1000 までの配列にそれぞれ適用しなさい」

 ここからは FizzBuzz で書いたプログラムを応用していく。PoA/DoA ともに、再利用する処理をメソッドでまとめている。DoA の例では class Array にメソッドを追加するというちょっと凝ったことをしているけど、ここでも、再利用のための修正量や記述量などそれほど大きな違いは無いように見える。

# ----------------------------
# PoA 「関数化(`・ω・キリッ」
# ----------------------------
def FizzBuzzOutput( x )
 if    x % 15 == 0
   puts "FizzBuzz"
 elsif x % 5  == 0
   puts "Buzz"
 elsif x % 3  == 0
   puts "Fizz"
 else
   puts x
 end
end
1.upto( 100){|i| FizzBuzzOutput( i ) }
1.upto( 200){|i| FizzBuzzOutput( i ) }
1.upto(1000){|i| FizzBuzzOutput( i ) }


# ----------------------------
# DoA 「関数化(`・ω・キリッ」
# ----------------------------
class Array
 def toFizzBuzz
   self.map{|i| ( i % 15 == 0 ) ? "FizzBuzz" : i }
 end
 
 def toBuzz
   self.map{|i| ( i % 5 == 0 ) ? "Buzz" : i }
 end
 
 def toFizz
   self.map{|i| ( i % 3 == 0 ) ? "Fizz" : i }
 end
end
puts 1.upto( 100).to_a.toFizzBuzz.toBuzz.toFizz
puts 1.upto( 200).to_a.toFizzBuzz.toBuzz.toFizz
puts 1.upto(1000).to_a.toFizzBuzz.toBuzz.toFizz


【応用2】「1000 までの場合だけ、30 の倍数の時に "FizzBuzzFozz" にしなさい」

 このあたりから、PoA で設計しているとコードの修正がやや面倒になってきている。実装形態としてもいくつか考えられるので、個人差が出やすくなりそうだ。一方、DoA での設計は、これまでの延長線上で素直にコーディングしている印象。

# ----------------------------
# PoA 「(・ω・;ん?」
# ----------------------------
def FizzBuzzOutput( x )
 if    x % 15 == 0
   puts "FizzBuzz"
 elsif x % 5  == 0
   puts "Buzz"
 elsif x % 3  == 0
   puts "Fizz"
 else
   puts x
 end
end
def FizzBuzzFozzOutput( x )
 if    x % 30 == 0
   puts "FizzBuzzFozz"
 else
   FizzBuzzOutput( x )
 end
end
1.upto( 100){|i| FizzBuzzOutput( i ) }
1.upto( 200){|i| FizzBuzzOutput( i ) }
1.upto(1000){|i| FizzBuzzFozzOutput( i ) }


# ----------------------------
# DoA 「問題なし!(`・ω・キリッ」
# ----------------------------
class Array
 def toFizzBuzzFozz
   self.map{|i| ( i % 30 == 0 ) ? "FizzBuzzFozz" : i }
 end
 
 def toFizzBuzz
   self.map{|i| ( i % 15 == 0 ) ? "FizzBuzz" : i }
 end
 
 def toBuzz
   self.map{|i| ( i % 5 == 0 ) ? "Buzz" : i }
 end
 
 def toFizz
   self.map{|i| ( i % 3 == 0 ) ? "Fizz" : i }
 end
end
puts 1.upto( 100).to_a.toFizzBuzz.toBuzz.toFizz
puts 1.upto( 200).to_a.toFizzBuzz.toBuzz.toFizz
puts 1.upto(1000).to_a.toFizzBuzzFozz.toFizzBuzz.toBuzz.toFizz


【応用3】「1000 までの場合だけ、標準出力でなく、hoge.txt に出力するようにしなさい」

 ここまでくると、PoA では修正がプログラムのほぼ全体に影響していることが明らかになる。一方、DoA はこれまでと異なる出力が必要な 1000 までの場合の周辺だけに修正を加えることで、所望の機能を実現している。

# ----------------------------
# PoA 「(´・ω・うーん、ちょっと苦しい」
# ----------------------------
def FizzBuzzOutput( x, io )
 if    x % 15 == 0
   io.puts "FizzBuzz"
 elsif x % 5  == 0
   io.puts "Buzz"
 elsif x % 3  == 0
   io.puts "Fizz"
 else
   io.puts x
 end
end
def FizzBuzzFozzOutput( x, io )
 if    x % 30 == 0
   io.puts "FizzBuzzFozz"
 else
   FizzBuzzOutput( x, io )
 end
end
1.upto( 100){|i| FizzBuzzOutput( i, $stdout ) }
1.upto( 200){|i| FizzBuzzOutput( i, $stdout ) }
file = open("hoge.txt","w")
1.upto(1000){|i| FizzBuzzFozzOutput( i, file ) }
file.close


# ----------------------------
# DoA 「問題なし!(`・ω・キリッ」
# ----------------------------
class Array
 def toFizzBuzzFozz
   self.map{|i| ( i % 30 == 0 ) ? "FizzBuzzFozz" : i }
 end
 
 def toFizzBuzz
   self.map{|i| ( i % 15 == 0 ) ? "FizzBuzz" : i }
 end
 
 def toBuzz
   self.map{|i| ( i % 5 == 0 ) ? "Buzz" : i }
 end
 
 def toFizz
   self.map{|i| ( i % 3 == 0 ) ? "Fizz" : i }
 end
end
puts 1.upto( 100).to_a.toFizzBuzz.toBuzz.toFizz
puts 1.upto( 200).to_a.toFizzBuzz.toBuzz.toFizz
file = open( "hoge.txt", "w")
file.puts 1.upto(1000).to_a.toFizzBuzzFozz.toFizzBuzz.toBuzz.toFizz
file.close


考察

 どうして、PoA と DoA でこのような違いが起きたか?その秘密は「モジュール性」にある。

モジュール - Wikipedia より:
プログラムのモジュールは、出来るだけ他のモジュールとの結合度を弱めて、独立性を高めることが望ましい。

 今回の FizzBuzz 問題も「15での剰余を求める」とか「画面に出力する」とか、数々のモジュールから成り立っているが、その結合の様子を図に表したものが以下になる。

 この図からもわかるように DoA では、結果的にプログラムにおける各モジュールの結合度を下げることができている。つまり、プログラム全体の再利用性が高まっているといえる。

思考は言語の影響を受ける…が、

 WEB+DB PRESS vol.60 の P.18 で Ruby の父・まつもとゆきひろ氏が、サピア=ウォーフの仮説を例に出して以下のように語っている。

「サピア・ウォーフ仮説」とは、「(自然)言語は、人間の思考に影響を与える」という言語学における仮説で、(中略) 長年プログラミング言語について考え続けてきた私には、この仮説は自然言語よりもむしろプログラミング言語においてよく成立するのではないかと感じられます。

 サピア・ウォーフ仮説は知らなかったけど、これは全くその通りだとおもった。
 いろいろな言語を知っていて、極めた人は「実装言語は関係ない」というかもしれないが、プログラミングの入りが BASIC ⇒ C 言語 だった私にとって、Lisp や Ruby の map の概念を理解したときは目から鱗だった。そんな考え方があるのか!と。

 Ruby では手続き的な書き方も、map を多用するような Lisp 的な書き方も、両方できる。そのため「よく使いそう」とか「再利用しそう」な『DoAでのマジ設計』のときと、「これは汎用性が低くてもいいからサクッと作ってしまいたい」という『PoAでの即席設計』の使い分けが柔軟にできるところがうれしい。そのぶん、Ruby で書いたからといってきれいに再利用性高く書けるというわけではない、ということにもなる。

 Ruby を学習することで、DoA での設計が自然に身に付き、Lisp脳に近づくことができたような気がしている。これからは Lisp や Haskell あたりもつまみぐいしながら、プログラミングのレベルをアップさせていきたいな。


おわりに

 本記事は、7年ほど前に「はてなダイアリー」で私が執筆したものを一部修正して掲載しました。当時は Ruby on Rails 全盛といった感じで、本記事も Ruby 全面推しとなっていますが、考え方自体はほかの言語でも応用可能ではと思っています。

 拙い文章と記事ではありますが、ツッコミ・ご意見などなどあれば、ぜひともお願いいたしますm(_ _)m


この記事が気に入ったらサポートをしてみませんか?