FizzBuzzプログラムは、プログラマを採用する時に全くコードを書けない人を足切りするために考案されたものだ。非常にシンプルで、汎用の高水準なプログラミング言語でコードを書ける人が慣れた言語を使えば、typoなどの細かいミスを除けば2〜3分程度で書ける。*1 *2
#!/bin/bash # fizzbuzz <number> fizzbuzz() { if (($1 % 15 == 0)); then echo FizzBizz elif (($1 % 3 == 0)); then echo Fizz elif (($1 % 5 == 0)); then echo Buzz else echo $1 fi } for ((i = 1; i <= 100; i++)); do fizzbuzz $i done
このシンプルさは、問題が単純であるだけでなく、現実のプログラミングでの泥臭さを剥ぎ取ったことの影響も大きいだろう。この程度でつまづくようでは、実際の開発でのプログラミングにはついていけない。*3
逆に言えば、現実のプログラミングにおける厄介さが加わることでFizzBuzzも難しくなる。君がプログラマであるかプログラマを目指している人ならば、FizzBuzzを書く時にこの点に留意すべきだ。
制御構造
君の書くFizzBuzzはいつもifやCスタイルのforといったお決まりの制御構造しか使用していないのか? それはよくないな。せっかく言語設計者がさまざまな制御構造・制御構文を用意してくれたのだから、もっと色々と使用してみるべきだ。
よくある手続き型のプログラミング言語では、ifやfor以外に2〜3の制御構文が用意されているものだ。C言語の三項演算子のように、制御構文ではないが限定された条件の下で制御構文の代用となりうる機能もある。これらの機能には向き不向きがある。例えばCスタイルのforはwhileよりも使用頻度が多いと思うが、forの「初期化、ループ継続条件、更新」スタイルに適合しないループではforよりwhileのほうが適切なこともある。このあたりの適切/不適切の判断は、色々な制御構文を使ってみるところから培われる。
言語によって特徴的な制御構造・制御構文もある。例えばPerl/Rubyのunlessやuntil、PowerShellにおけるパイプラインとForEach-Objectによるコレクションの処理などだ。これらの機能の良さや他の制御構文との住み分けは、とにかく何度も使ってみなければ分からないものだ。分かってくることで、その言語らしいソースコードの記述に一歩近づくだろう。
制御構文それぞれの特色を理解し、使い分けるにも、とにかく一度は触ってみるべきだ。FizzBuzzを書くときも、いつものお決まりの制御構文だけでなく、さまざまな制御構文を試してみるべきだろう。
#!/bin/bash # fizzbuzz <number> fizzbuzz() { local s (($1 % 3)) || s=Fizz (($1 % 5)) || s=${s}Buzz [ "$s" = '' ] && s=$1 echo $s } for i in {1..100}; do fizzbuzz $i done
データ型
君のFizzBuzzは整数値と文字列程度しか使っていないのか? それは不十分極まりない。データの構造化はプログラムの全体像を決定づける重要な作業だ。その選択肢を増やすためにも、言語機能に用意されているデータ構造化のための様々な機能を使うべきだ。
電子計算機であるコンピュータだが、現在では純粋な計算のために使用している分野は限られている。大抵の分野では、コンピュータは情報処理のために用いられている。
情報──データは重要だ。手続き型のプログラミング言語で処理を記述したとき、実行時に実際に行われる処理の内容を決定づけるのはデータだ。ソースコードがプログラムの静的構造を表すものだとして、実行時に静的構造の中のどのルートを辿るのか? その答えはデータの中身次第だ。
またデータをどう見なし、どのように扱うか──データの構造化については40年以上前から研究されている。プログラムはアルゴリズムとデータ構造が緊密に絡み合った代物だ。データ構造の選択によってアルゴリズムは変化し、それによって少なくとも手続き型言語によるソースコードの記述も大きく変化する。適切なデータ構造を選択すればプログラムは易しくなり、誤ったデータ構造を選択すれば恐ろしく複雑になったり問題を解決できなくなったりするものだ。
システム開発の比較的ミクロや領域においては、現在でもデータをどのように構造化するかは重要なポイントだ。そしてデータ構造化をうまく行うためにも、使用する言語にデータ構造化に関わるどのような機能があり、どのように組み合わせればよいのか、知っておく必要がある。
たかがFizzBuzzと油断することなく、データやデータ型について色々な機能を試しておくべきだ。それによって、ミクロな領域における設計技法の幅が広がるだろう。
#!/bin/bash declare -a fbtbl=(number Fizz Buzz FizzBuzz) # fizzbuzz <number> fizzbuzz() { local -i idx=0 (($1 % 3 == 0)) && ((idx++)) (($1 % 5 == 0)) && ((idx += 2)) fbtbl[0]=$1 echo ${fbtbl[$idx]} } # fbwrapper <index> <line> fbwrapper() { fizzbuzz $2 } printf '%s\n' {1..100} | mapfile -c 1 -C fbwrapper
サブルーチン、モジュール機能
君のFizzBuzzは単体のアプリケーションなのか。それでよいのか? 金輪際その言語ではFizzBuzzを書かない? たとえそう誓ったとしても、それが確実に守られる保証は無いというのに。
構造化プログラミングの頃から、プログラマはソフトウェアをモジュール化してきた。大きすぎるものを正しく理解することは困難だ。分割統治の精神に従い、理解できる大きさのモジュールに分割することになる。古くはサブルーチンが、今ではクラスやパッケージがモジュールとして扱われている。
モジュール化の第一義は分割統治だ。つまり、たとえ1度しか呼ばれない機能であっても、システムの規模やその機能の独立性からみて妥当であると思われるなら、あるコード片をサブルーチンやクラス・パッケージとして独立させることをためらう理由はない。
とはいえ、系統的な分割統治の結果としてシステムの複数の個所に似たようなモジュールが存在するよりは、1つのモジュールにまとめて複数ヶ所から呼び出すほうがよい。モジュールの再利用性は、大きなシステムを開発する際に考慮するべき要素の1つだ。
もっとも『人月の神話』での指摘を鑑みれば、おそらく再利用可能なモジュールの開発には最低でも通常のデバッグ済みプログラムの3倍のコストがかかることになる。なので、安易に「モジュールの再利用」を訴えるべきではないだろう。
現実のシステム開発では、普通にコードを書くだけでもサブルーチンやクラスなどのモジュールの類を実装することになるだろう。または複数の開発で使用される汎用のモジュールを設計・実装する機会もあるかもしれない。
その時になってまごつかないように、言語仕様として用意されているモジュール機構を試してみるべきだ。同時に「よいモジュール」や「よいインタフェースをもつモジュール」とは何か考え、学習し、実践しておくべきだろう。
このことはFizzBuzzでも同じだ。むしろFizzBuzzだからこそ、あえて解くべき問題から離れて「モジュール化」に集中してみるべきだろう。解くべき問題が簡単であるからこそ、非本質的部分に集中する余裕があるはずだ。
# fizzbuzz_module.bash # To use this function, you need bash(1). # fizzbuzz <number> fizzbuzz() { local s (($1 % 3)) || s=Fizz (($1 % 5)) || s=${s}Buzz [ "$s" = '' ] && s=$1 echo $s }
#!/bin/bash . "`cd \`dirname $0\`; pwd`/fizzbuzz_module.bash" for ((i = 1; i <= 100; i++)); do fizzbuzz $i done
Unixフィルタ・スタイル
君のFizzBuzzは単に結果を出力するだけで、入力は受け付けないのか? それは問題だ。プログラミングで成す物事の多くは情報処理だ。入力を受け付け、加工し、出力する。ならばFizzBuzzもそうあるべきだ。
プログラムが外部からの入力を受け付けるようになると、外の世界には危険が待ち受けていることに気がつくはずだ。もし正の整数値の文字列表現を入力として受け付けるプログラムを書いたなら、現実には負の値、小数部を含む値、大きすぎる値、アルファベットや記号を含む値、先頭に `+' が付いた値、2進数/8進数/16進数表記その他の文字列表現が入力されるだろう。そもそもテキスト以外が入力されるかもしれないし、もしかしたら何も入力されないかもしれない。これらの値は誤って入力されることもあれば、悪意を持って入力されることもある。
予期せぬ値を入力されたらクラッシュしてしまうのか、クラッシュはしないがアプリケーション内部のどこかで妙なことが起きて後で表面化するのか? それとも無視するかエラーとして弾くなどして適切に処理するのか? そもそもどこまで入力として許容するのか? これは「腕の見せ所」とかそういうレベルの問題ではない――プログラマの義務レベルの内容だ。*4
差し当たりUnixの世界では伝統的なテキストフィルタ・スタイルのアプリケーションとしてFizzBuzzを実装してみるとよいだろう。フィルタは単純なプログラムだが、入力のチェックと変換が必要になる。それに作業中ちょっとした処理を自動化したくなった時、フィルタを書けると便利なことも多い。慣れておいて損はない。
#!/bin/sh # To run this script, you need gawk(1). exec gawk ' function fizzbuzz(n, s) { if (n % 3 == 0) s = "Fizz" if (n % 5 == 0) s = s "Buzz" if (s == "") s = n return s } /^[ \t]*[1-9][0-9]{0,8}[ \t]*$/ { print fizzbuzz($1) next } { print "Invalid value: \"" $0 "\"" > "/dev/stderr" }'
対話型ユーザインタフェース
君のFizzBuzzは非対話型のコンソールアプリなのか。よく周りを見てみろ。WindowsにMac、iPad、iPhone、Android――どれも対話型のGUIを備えている。そろそろGUIとは言わなくとも対話型のFizzBuzzも必要だと思わないか?
ユーザ(人間)と相対するフロントエンドのインタフェースの多くは対話的に操作するように作られている。現在では大半がGUIで構築されているが、CUIでも対話型インタフェースは構築可能だ。フロントエンドを作るなら(プログラマとして)望む・望まないに関係なく対話型のインタフェースを要求されるだろう。
フィルタのような形式のコンソールアプリと異なり、対話型アプリではイベント駆動型のスタイルが求められる。フロー駆動型とは少々異なるスタイルだ。現実には対話型アプリ以外でもイベント駆動型のスタイルが求められるので*5慣れておいて損はない。それに対話型アプリに関わると否応なく「0.1秒ルール*6」を学ぶことになる。ユーザに「固まった!」と思わせたらマズイのだ。
あと対話型アプリの良いデザイン、特にGUIのデザインを考えるのは難しい。少なくとも徒手空拳で立ち向かえる相手ではない。素人なりに知識を仕入れて理論武装した方がよい。実際に対話型アプリを書いてみるとそのことを実感する(人も多い)だろう。
ひとまず使い慣れた言語環境で手頃なGUIツールキットを弄ってFizzBuzzしてみるとよいだろう。こだわりがなければTkあたりが手頃だ。手持ちの言語から(叩けるなら)叩いてもよいし、いっそTclで書いてしまうのもありだ。JavaScriptの読み書きに不自由しない人ならWebブラウザでもよいだろうし、Windows限定の「HTA + Jscript」という亜種もある。WPF + XAML + PowerShellでも十分かもしれない。
CUIでも構わないなら、例えばncursesやPDCursesベースのライブラリを叩く方法がある。シェルスクリプトで手軽にいきたいならdialogだろう。
#!/bin/sh # To run this script, you need dialog(1). readonly progname=`basename "$0"` trap 'rm -f "$tmpfile"; exit 1' 1 2 15 tmpfile=`mktemp "$progname.$$.XXXXXXXXXX"` if [ $? -ne 0 ]; then echo "$progname: cannot create temporary file" 1>&2 exit 1 fi while : ; do dialog --inputbox 'Please input FizzBuzz max number (1 - 1000)' 0 60 100 2>"$tmpfile" [ $? -eq 0 ] || break count=`cat "$tmpfile"` [ "$count" -eq 0 ] 2>/dev/null if [ $? -ge 2 ]; then dialog --msgbox "Not number: '$count'" 5 60 elif [ "$count" -lt 1 -o "$count" -gt 1000 ]; then dialog --msgbox "Out of bound: $count" 5 60 else dialog --infobox 'Now creating FizzBuzz answers. It will take some time.' 5 60 seq 1 "$count" | awk '{ s = "" if ($0 % 3 == 0) s = "Fizz" if ($0 % 5 == 0) s = s "Buzz" print s == "" ? $0 : s }' >"$tmpfile" dialog --textbox "$tmpfile" 0 0 fi dialog --yesno 'Continue ?' 5 60 [ $? -eq 0 ] || break done rm -f "$tmpfile" clear
クライアント・サーバ型ネットワークプログラミング
君のFizzBuzzはそのデバイスの中で動作するだけなのか? クラウド時代を見据えたFizzBuzzソリューション*7としてそれはいかがなものか。
最近は良くも悪くもネットワークに接続してTCP/IPを喋る機器が一般化している。その昔は自宅でネットに繋がっている機器といえばPCぐらいだったのが、今ではガジェットもテレビ+各種レコーダも音響機器も楽器もネットに繋がっている。PC上のアプリケーションでさえも自動更新機能の類を備えるようになって久しいこのご時世、ごく普通のアプリケーション・プログラマでも通信プログラムの類を書く機会があるだろう。
それだけでなく、アプリケーション自体もWebベースのサービスが増えている。デバイス上で動作するアプリケーションにも、特定のWebサービスと連携するようなものが多くなってきた。
ともかく最近は何でもかんでもHTTPベースだ。FizzBuzzも、まずはHTTPによるクライアント・サーバモデルから始めてみるべきだろう。もし音声や動画のストリーミングに技術的な興味があるのなら、素のTCPやUDPでFizzBuzzするのもよいだろう。TCPでの経験はWebSocketにも応用できる。UDPを使えば、通信の信頼性とリアルタイム性能のトレードオフに気づくかもしれない。
#!/bin/sh # To run this script, you need curl(1). exec curl -s 'http://192.0.2.11:8080/fizzbuzz?count=100'
#!/bin/sh # To run this script, you need Open usp Tukubai. PATH=/usr/local/bin:$PATH # cgi_header <HTTP status> [additional header field...] cgi_header() { local line echo "Status: $1" echo 'Content-Type: text/plain; charset=utf-8' shift for line in "$@"; do echo "$line"; done echo } # do_exit <status> do_exit() { rm -f "$tmpfile" exit $1 } # error <HTTP status> [additional header field...] error() { cgi_header "$@" echo "HTTP $1" do_exit 0 } trap 'rm -f "$tmpfile"; exit 1' 1 2 15 tmpfile=`mktemp "/tmp/$$-XXXXXXXXXX"` [ $? -eq 0 ] || error '500 Internal Server Error' [ "$REQUEST_METHOD" = GET ] || error '405 Method Not Allowed' 'Allow: GET' echo "$QUERY_STRING" | cgi-name -d_ -i_ >"$tmpfile" count=`nameread count "$tmpfile"` [ "$count" -eq 0 ] 2>/dev/null [ $? -lt 2 ] || error '400 Bad Request' [ "$count" -ge 1 -a "$count" -le 1000 ] || error '400 Bad Request' cgi_header '200 OK' seq 1 "$count" | awk '{ s = "" if ($0 % 3 == 0) s = "Fizz" if ($0 % 5 == 0) s = s "Buzz" print s == "" ? $0 : s }' do_exit 0
複数言語
君は使い慣れたいつもの言語でFizzBuzzしているのか? それだけでは不十分だ。複数のプログラミング言語で──普段使いの言語以外でもFizzBuzzするべきだ。
分野にもよるが、昨今のシステム開発では複数のプログラミング言語を使用することが多い。Web開発ではクライアントサイドとサーバサイドとで使用する言語が異なることはざらだ。モバイル分野では、ライブラリはC言語やC++で共通化しつつUI側は各プラットフォーム向けの言語を使用する、なんてアプローチをとることもある。PCアプリケーションでは、場合によってはWindowsだけでなくMacのことも考えることになる。組み込み分野では、基本はC言語を使いつつも、パフォーマンスの都合でアセンブラを併用したり、GUIの実装にC++を併用したりする。
実際に開発に使用する言語以外にも、手元でのちょっとした作業にスクリプト言語やミニ言語(DSL)の類を使用することは多い。
ともかく最近のプログラマは複数のプログラミング言語を扱うものだ。ならばFizzBuzzも複数言語で……という発想は不自然ではない。
とはいえ何でもよいからいくつかの言語でFizzBuzzすればOK、という訳でもない。例えば君がC言語・C++・C#・Java・JavaScript・Objective-CでFizzBuzzを書いたとして、どれも似たようなプログラムだったならば意味がない。C言語のスタイルそのままでJavaScriptのコードを書いたなら、それを見たJavaScript経験者に「君はJavaScriptことを何も分かっていないね」と言われるだろう。もうちょっと大きなコードを書けばボロがでるだろうし、自分の足を撃ち抜いてしまうかもしれない。コの業界ではこういう現象を「BASIC臭いC言語のコード」とか「コボラー臭いJavaのコード」などと称している。
何となく見た目が似ていても、言語が違えば扱い方は変わる。C言語になれた人が何も考えずにC++やObjective-Cで書いたFizzBuzzは、C言語でのそれと代わり映えしないだろう。しかしC++やObjective-Cに通じた人がその言語らしさを発揮させたFizzBuzzは、C言語で記述したC言語らしいFizzBuzzとは異なるものだ。
だから、複数の言語でFizzBuzzを書くのなら、使用する言語らしさを追求するべきだ。
もしより高い学習効果を狙うのなら、普段使っている言語とはパラダイムの異なる言語でFizzBuzzしてみるべきだろう。
multiple(X, Y) :- X mod Y =:= 0. answer('FizzBuzz', N) :- multiple(N, 3), multiple(N, 5), !. answer('Fizz', N) :- multiple(N, 3), !. answer('Buzz', N) :- multiple(N, 5), !. answer(N, N). fizzbuzz(N, Max) :- N > Max, !. fizzbuzz(N, Max) :- answer(V, N), writeln(V), M is N + 1, fizzbuzz(M, Max). fizzbuzz(Max) :- Max >= 1, fizzbuzz(1, Max).
高水準言語は、抽象化されたコンピュータそのものだ。抽象化された姿は、言語によって異なる。ならば、抽象化された姿に合致したコードは、言語によって異なるはずだ。
複数の言語を学ぶこと、とりわけパラダイムの異なる言語を学ぶということは、抽象機械としてのコンピュータを複数種類弄くるということだ。抽象機械が異なれば、その抽象機械の中の理も異なる。異なる言語を学ぶということは、異なる理を学ぶということ。だから結果として視野が広がるのだ。
*1:アセンブラや用途が限定されたミニ言語(例えばsedとか)は除外する。実際、CASL IIやバッチファイルでFizzBuzzを書いた時は結構時間がかかった。
*2:「どうしてプログラマに・・・プログラムが書けないのか?」を読むと「2分とかからずに紙に書き出せる」という記述がある。恐らく採用面接にてその場で紙に書かせていたのだろう。この場合、細かいtypoには目をつぶっていたはずだ。なぜなら実際にエディタでコードを書く際は入力補完を使うし、文法の細かい間違いは例えばコンパイラやLintの類で洗い出すからだ。
*3:もっともFizzBuzzの役目はあくまで「足切り」だ。FizzBuzzを書けることが現実の開発に通用する技術力を持っている証拠とはならない。
*4:正確には「最大限努力すべき『努力目標』」とでもいうべきか。残念ながら現状では「100%安全」は微妙に保証できない。では「99.9%安全」なら――時間的・金銭的コストとの兼ね合いで変動するだろう。
*5:例えばネットワーク・プログラミングの受信側。また何らかのプラグインを実装する場合、そのインタフェースがイベント駆動のコールバック形式なことも多い。
*6:『プログラミングWindows 第5版 下』P.492より。メッセージキューを処理するスレッドでは1つのメッセージを処理するのに0.1秒以上の時間をかけないようにするべき、ということ。Windowsアプリの場合、0.1秒以上かかりそうな重い処理は別スレッドに分割する。
*7:それが何なのかは書いている私ですら不明。