はてなキーワード: 連想配列とは
一理あるご意見をいただきました。ありがとうございます。
モンスター.attackedBy(勇者.attackPower)
誤解を招いてしまったようですいません。先の理由1では、"能動態のほうが受動態より解りやすい"、ということを主張したかったのです。
あとづけで申し訳ないのですが、その順序で書くのにはもう一つ、MVC的な理由があります。
RPGの戦闘シーンの入力Controllerは、ユーザーのメニュー選択等によって、勇者の攻撃方法を攻撃対象モンスターを決定し、攻撃を実行する。
つまり、この入力Controllerがコントロールする対象は、勇者と彼の攻撃対象だ。
勇者.attack(モンスター)
と書くと、勇者に命令していることが明示されるので、その設計意図がはっきりする。
なるほど、その設計は思いつきませんでした。〈モンスターへの攻撃と勇者への攻撃を対称に捉える〉という視点に目からウロコです。
ひとつめの質問は、攻撃方法型は、具体的にはどんなメソッドやフィールドが並ぶのか、です。
考えてみたのですが、どうしても"通常攻撃","ディレイ攻撃","attackPowerの値","すばやさ", などが並ぶ、ディクショナリ(連想配列)的なものが思い浮かんでしまいます。そうすると、結局モンスターのattackedByメソッドにif分岐を並べることになってしまうので、増田さまお考えの設計とは違うのでしょう。
モンスター.attackedBy(攻撃方法型: ジャンプ攻撃)
の方が良い利点はなんでしょうか。私は"時間的な処理の流れがわかりやすい"設計になりやすいことを主張しました(理由2参照)・
増田さまが考えるほかの利点があればぜひ教えてください。
ご意見くださって、ありがとうございます。
例にあげたRPGのモデル設計はhttp://detail.chiebukuro.yahoo.co.jp/qa/question_detail/q1178047006を参考にしました。
ですが自分で書くとしても、十中八九そのような設計にするでしょう。
モンスター.attack(勇者.attackPower)
勇者.attack(モンスター)
主語.動詞(目的語)
モデルに対する命令は、ふつう入力Controllerのイベントハンドラに書かれる。attackに関する命令はどちらの設計をとったとしても"攻撃が選択された"イベントハンドラに書かれるだろう。
モンスター.attack(勇者.attackPower)
だと、たとえばモンスターにダメージを与える要素として、他に「すばやさ」などを加えようとしたら、モンスターのattackメソッドを変更する必要が生じる。
また、FF5には"竜騎士のジャンプ"という攻撃方法があった。ジャンプを選択すると、竜騎士は空高く飛び、一定時間が経ったのちに落ちてきて、空中からモンスターを攻撃する、というもの。これを実装しようとすると、入力Controllerのイベントハンドラの直前の部分で別スレッドに投げるなり、タイマーを起こすなりして処理したくなる。
味方同士の”連携攻撃”を実装しようとするとやはり、入力Controllerのイベントハンドラを修正したくなるだろう。
つまり、悪い方の設計ではModel「勇者」への設計変更の都合で、Model「モンスター」や入力Controllerを修正する必要が出てきてしまう。
勇者.attack(モンスター)
では、どの設計変更の例でも、勇者側を変更するだけで良い。この設計には、クラス間の責任の分担がはっきりするという利点がある。
list.append(elem)でしょ。
リストは実質的にはオブジェクトというより、ただのデータ構造。
リストやディクショナリ(連想配列)は便利な道具だが、これを前面に押し出した設計は複雑さを招く場合がある。
たとえば、"複数のモンスターの集まり"や"ソートされたリスト"を、リストを継承して実装しようとすると、とたんにドツボにハマる。
Onスライムが仲間を呼んだ(){ スライムの集まり.append(スライム) スライムの個数ラベル.更新(スライムの集まり.length) if(スライムの集まり.length == 1 and スライムの集まり.get(0).name == "キングスライム"){ スライムの名前ラベル.更新("キングスライム") } }
スライムは8匹集まるとキングスライムになるので、最初の一行でスライムの集まりを変更して以降、スライムの個数を気にする必要があることに注意して欲しい。
上記のコードを洗練させるために、Model「スライムの集まり」で個数が変わった時にイベント"OnLengthChange"を発行するようにしても良いが、そのようにすると、lengthメソッドを安全に呼べるのはそのイベントハンドラのみ、ということになる。プログラマに注意を促すために、lengthメソッドのコメントにその旨を注意書きしておく必要がある。
"スライムの集まり"リストクラスは、プログラマが期待するだろうリストクラスの振る舞い《append後の個数はappend前+1》を破壊している。
"ソートされたリスト"は、appendメソッドをオーバーライドして、追加時に勝手に順番を並び替えてくれるようなものだが、これも《リストに要素を追加した時には、その要素は末尾に置かれる》というリストクラスの振る舞いを破壊する。スライムの例と同じように、イベントの発行とメソッドの注意書きが必要になる。プログラマはそれらのクラスを使うときに、より注意深くならなくてはいけない。
このように、クラスの振る舞いを壊してしまうようなクラス設計は、プログラマを地雷原におくりこむことになる。
とはいえ、リストのようなデータ構造を便利に使えるシーンもある。
はじめからおわりまで100行程度のスクリプトで、とくにイベントドリブンにするほどの複雑さがない場合。順序を追って読めるので、時間的な流れがわかりやすい。
一方、RPGの例で示したような設計は、オブジェクト指向をフルに使って、コードを責任に応じて役割分担しやすくなる利点がある。
前者は"トランザクションスクリプト"的設計と呼ばれ、後者は"ドメインモデル"設計と呼ばれる。
自分はJavaからPHPに入ったので、最初はとにかくクソな言語だと思っていた。
Copy On Writeという謎仕様なのに加え、すべての配列が常に「順序付きマップ」という謎なものになっている。ただ単にマップ(連想配列)を使いたいだけの場合でも、なぜが挿入順でキーが順序付けされているという仕様で、内部でどんだけオーバーヘッドがあるんだろうと考えると、それだけでストレスが溜まったものだ。
あと、何も宣言せず、$array['key1']['key2'] = 1としただけで、要素が1の配列が2つ作られる。これも気持ち悪い。この仕様のせいで、どれだけ見つけづらいバグが誘発されているかと思うと、それだけで痒くなった。
そんな風に思いながら、2年近く仕事をして、そこそこ大規模なシステムを一人で書けるようになった。それでも、PHPをやっている引け目のようなものはなくならなかった。
ただ、そんな思いのまま「意識が高い」系のプログラマが集まる会社に移り、RubyだとかPythonだとかScalaだとかが書けるようになった上で、あらためてPHPを見ると、その仕様が優れていることを痛感させられる。同じことを書こうとしたときの行数が圧倒的に少ないのだ。配列についてもたしかに、オーバーヘッドはあるが、実際にはパフォーマンスに影響することは少なく、それを通して得られる開発効率は半端じゃない。他の言語だといくつものライブラリを介して可能になることが、単にサーバにファイルを置くだけで可能になるというのは、特に小規模なプロジェクトでは生産性の高さに直結する。
もちろん、プロセス/スレッド/データ共有の仕組みが前時代的で、パフォーマンス効率は高くない。言語仕様が、ノンブロッキングな処理に向いていないなど、用途によっては致命的な欠点もないわけではない。ただ、きちんと言語の特性を理解するレベルで使ったことがないのに、仕様的に他の言語と変わっているところを挙げて、「PHPを使うのはレベルの低いエンジニア」というのは、そろそろ終わりにした方が良いと思う。
あと、話は変わるが「意識が高い系エンジニア」は、システムを開発する上で人件費や採用コストの問題をあまり考えていないのではないかと思う。たとえば、ビジネスが急に大きくなって、取り急ぎ100人エンジニアを雇おうとなったとき、PHPならとりあえず書ける人間をかき集めやすい。RubyやPythonで同じことをやると採用にかかる時間が大幅に伸びるか、人件費が大幅にアップするかになるだろう。これは、一緒に働くエンジニアを所与の条件として見るか、お金を払って雇うべきダイナミックなものとして見るかの違うじゃないかと思う。
Pixearch(ピクサーチ)
node.jsとMongoDBの勉強がてらpixivの画像のタグ検索のサービスを作りました。
はてブのタグ検索のようにpixivに投稿された最近の画像をpixiv内でのブックマーク数でフィルタをかけて検索できるのが特徴です。
検索したり、タグをたどって、ダラダラと良い絵を眺めるのを目的としています。
普段はブログを書いたりしてないので、今回学んだことのメモがてらの投稿です。
MongoDBを試そうと思ったのがサービスを作り始めた発端です。
Web部分はmongoDBと相性が良さそうなnode.jsを採用。
MVCのフレームワークで何か適当なものはないかとググってSails.jsが良さそうだったので今回採用しました。
今回はせっかくnode.jsを採用したので、噂のnode.jsのPaaSのnodejitsuを試しに使っています。
500MB分の容量のMongoDBを最初から使えるのも大きかったです。
とりあえず最小のプランにしてるのでどのくらい捌けるのか気になるところ。
Web開発用のモジュールを自分で用意するのがめんどいなー、という人向けな印象。
このくらいの規模のものだったらサクッと作れました。
ただある程度の規模のちゃんとしたサービスを作るのには色々足りてないので、自分でカスタマイズしたりできる人じゃないと使うのは辛そうです。
後、ドキュメントも公式のものだけだと説明されてない機能が結構あったりします。
デフォルトだとDBはMySQLに対応していて、MongoDBを使うにはsails-mongoを入れる必要がありました。
開発中に困ったことは、nodejitsuで動かそうとしてsails-mongoでエラーが出て、調べてみたらauthenticationに対応していないというバグがあったことでした。
手元で直したのでpull requestを送ろうかと思ったら既に他の人が送っていて、3日前ぐらいに取り込まれているので今は大丈夫なはず。
https://github.com/balderdashy/sails-mongo/pull/36
現在進行形で色々Issueが上がって修正がされているのでそのうちこなれてくるのに期待。
細かいところで設計や考慮がちゃんとされてるなーと感じたところも多かったので、node.jsで開発してる人は一回試してみると勉強になりそうです。
最初にコレクションの操作に戸惑ったのですが、結局JSの連想配列なので思ったより早く馴染みました。
$setとか$gteとか特殊な意味を持つキーがいくつかあるので、その辺を把握できてから色々と捗りました。
MySQLに比べて特に更新系で複雑なクエリが発行できるので、ORMで使うと十全に機能を発揮できないのではないかな、と思ったり。
ご存知スキーマレスなので、何も考えずにデータを突っ込んでるとIntegerで保存したい値がStringになっててソートのときに困ったりするので要注意。
指定した容量を超えたら自動で消してくれるCapped Collectionがあると知ったので、今回みたいな容量が限られてる場合に便利かなと試してみたのですが、このオプションを有効にしたコレクションだとデータのアップデートや削除ができなくなりました。
おそらく、追加しかしないログのようなデータの保存に使うもののようです。
まだ微妙な部分も多かったですが、デプロイとかコマンド一発でできて、設定管理がpackage.jsonでできたりして面白かったです。
今回は、特に問題が起きたときに環境にsshで入ったりできないので、表示されてるログだけで問題を調査するのに苦労しました。
MongoLabとMongoHQというMongoDBの外部ホスティングサービスが上述したように使えるのですが、無料の容量を超えて使うにはそこそこお金が掛かるのでモリモリ容量を使うものを考えている場合は注意がいります。(もちろん値段に見合ったプロダクトを提供してくれると思いますが)
ということで、せっかく作ったので是非試してみてください。
http://1-byte.jp/2011/03/20/20_tips_you_need_to_learn_to_become_a_better_php_programmer/
良いPHPerだって?そんなものは丸めてゴミ箱にでも捨ててしまった方が資源の再利用になる分いくらかマシだ。
つまり俺たちがしなくちゃならないことは「より良いPHPerにならないため」に何ができるかってことなのさ。
それじゃ、始めよう。
?>なんて使っちゃいけない。そう俺たちはBAD PHPer。
無駄なホワイトスペースの出力に悩まされるくらいなら対称性なんて丸めてゴミ箱にでも捨てた方がまだマシだ。非対称性こそが賛美。
require_once("config.php");
未だにこんなことやってるやつがいるのかいベイベー。絶対にダメだ。この一行を見たら俺は悶絶する。
ダメだ、早く何とかしないと。
大抵このconfig.phpの中身はこうなっている。見て絶望だ。
$hoge_path = ''; if (!LOCAL) { define('FOO_FLAG', 1); if (HONBAN) { define('HOGE_FLAG', 1); } else if (TEST) { define('HOGE_FLAG', 2); } } else { $hoge_path = '/local'; define('FOO_FLAG', 2); define('HOGE_FLAG', 3); } define('HOGE_URL', $hoge_path.'/hoge/');
こういうのが延々と続くわけだ。もういやだ。もう見たくない。
本番環境とテスト環境でどういう値の違いがあるのか、ローカル環境だとどうなるのか、まったく把握できる気がしない。
なまじPHPな設定ファイルのせいで、処理をついつい書いてしまう。そしてどんどん複雑になってしまう。
やはり設定データは基本的にYAML等のデータしか定義できない形式のもので用意すべきだ。そして環境ごとに設定ファイルを分けるべきである。
そうすることで何にどういう違いがあるのかすぐにわかるし、diffすれば一度にすべて把握することができる。
# 本番環境設定ファイル foo_flag: 1 hoge_flag: 1 hoge_url: '/hoge/'
# テスト環境設定ファイル foo_flag: 1 hoge_flag: 2 hoge_url: '/hoge/'
# ローカル環境設定ファイル foo_flag: 2 hoge_flag: 3 hoge_url: '/local/hoge/'
// ここで後の処理のためにhogeメソッドを呼び出しておく $q->foo(); // $a['foo']はここに来る時点で真のはず // 2010-03-10 判定がおかしいので修正 // 2010-06-21 やっぱり値が入ってる方が正しい if ( !isset($hoge[0]) ) { }
コメントは保守されない。そう、それは真実。こんなコメントを発見したら即効削除しよう。コメントは基本信じるな。
俺たちにちょっとしたヒントと大きな損害を与えてくれる、それがコメントの役割なのだ。
わかる。いいたい事はとてもわかる。俺たちはしばしばインデントにスペースを使うはずだ。一方でIDEのしっかりした言語ではタブも使うことがある。しかし悪いことに、両者を混同しているプログラマも一定数いるのだ。
タブを画面上で認識しにくいエディタが世の中には存在する(何とは言わないが)
そして画面上で認識しにくいことを理由にタブを気にしないプログラマがいる。
この二つの条件が重なると、タブとスペースの交じり合ったインデントが完成する。もうぐちゃぐちゃだ。これは永遠に続く戦いだ。
私たちが勝利を掴むためにできることなどせいぜい、常にスペースしか使わない。タブを見つけたらその都度スペースに変換する。そういった地道な活動が明日へとつながるのだ。
われわれがプログラムをするとき、何に一番時間がかかってるか。実は変数の命名なのである。ここで拘り過ぎて時間をかけ過ぎては何も進まない。
御託はイイからさっさと書け、だ。しかしとはいっても変数名は重要。日頃からどういうときにどんな名前を使うかを決めておくといい。
そして変数名に型はまったく必要ない。型宣言のないPHPにおいて、型の変数名をつけること自体ナンセンスだ。
$iNumber = 'aaa';
になんの意味もない。コメントを信じるなでも言ったが、これはプログラマを混乱させるだけの害悪なものだ。
変数を使う前に初期化するのは、警告を出さないという意味でも良い癖だ。しかし具体的にどこでやるかが問題だ。
$foo = null; $foo = $q->foo();
こんな初期化に意味はない。よくあるのはやはり、if文で値を振り分けるケースだろう
$foo = null; if ( $hoge ) { $foo = 1; } else if ( $bar ) { $foo = 2; }
このときの初期化はとても有効だ。もしnullの初期化を忘れたまま$fooを使うと警告が出るが、ちゃんと初期化してるので出ない。基本中の基本だ。
function getStatus() { $bReturn = false; if ($i == 2) $bReturn = true; return $bReturn; }(中略)
もし、何かしらの理由で、あなたの書いたif文が間違っていたら?
この書き方をしていれば、間違った値に対して、常にfalseが返る。
私たちが、PHPでsensitiveなデータを取り扱うなら、正しいデータが入力されるまでは、動かないコードを書くべきだ。
trueとfalseの条件がいまいち明確ではないが、本当に動かないコードを書けというのであれば以下のようにすべきだ
function getStatus() { $bReturn = false; if ($i == 2) $bReturn = true; else if ($i == 1) $bReturn = false; else throw new Exception("bad status! $i"); return $bReturn; }
中途半端にfalseを返して生存させる必要性はまったくない。今すぐ死ね!
連想配列のキーを指定する場合だけ定数と間違わないようにクオートで囲まなければならない。そして逆に定数を使いたい場合はクオートで囲ってはいけない。
更に後世のプログラマが処理を見たときに、定数が使いたかったのか、文字列が使いたかったのかを明確にしたい場合はconstantを使うと良い。
// 定数のFOOを使うよということが明確になる print $a[constant('FOO')];
もし、文字列を変数の値と一緒に出力するとき、PHPではコンマの代わりにprintfを使うことが使える。
printf( “Hello, my name is %s“, $sName);
以下の代わりに上記のコードを使う。
echo “Hello, my name is “, $sName;
出力すべき変数が増えれば増えるほど、有効になっていく。とにかく迷ったならば、printfを使え、だ。
三項演算子はとても有効だ。しかし優先順位に難があるせいで、三項演算子をネストしようとすると以下のようなコードになってしまう
$n = (($i == 1) ? 2 : (($i == 2) ? 3 :$i));
括弧だらけで読みにくいったらありゃしない。三項演算子を使うなら一回まで。約束守れないやつは丸めてゴミ箱にでも捨てちまえ。
if ( $flag ) { }
仕様をちゃんと把握しているなら真偽値のチェックなどこれで十分。
もし事前にbool型だというのが確定してるのなら「$flag === true」を使えばいい。
インクリメント、デクリメント演算子は前に付くか後ろに付くかで意味が変わるので慣れるまでは非常にややこしい。
わけがわからなくなるくらいなら初めから使わないほうが良い。見極められないなら使うな。それがPHPerなのだ。
文句なしだ。これは文句がない。
他にも色々あるので覚えておこう
$a %= 1; $a &= 1; $a |= 1; $a ^= 1; $a <<= 1; $a >>= 1;
てっとり早く画面に表示する際にpreはよく使うが、デザインの関係上画面の文字が見えないときがある。
なのでdivを使って以下のようにしとくと便利だろう。
function p($var) { echo "<div align='left' style='background-color:white;color:black;'><pre>"; print_r($var); echo "</pre></div>"; }
君らが通常作るアプリケーションなんぞに、定数なんぞ必要ない。いいか、もう一度言う、お前ら程度のもんが、定数使おう何ぞ、おこがましいわ!
大丈夫。なんでもかんでも定数にする必要はない。結局設定ファイルに定数をずらずら作りまくってわけがわからなくなってるパターンが多い。
貴様みたいなもんに、定数は制御できん。いいか設定ファイルはYAML等のデータで持つようにし、その連想配列のデータ構造を一つ持ってるだけで定数の変わりになる。
このメリットに比べれば、定数だと書き換えられなくて良いという利点などこの歯のカスほどのものだ。そんなものは丸めてゴミ箱へ捨ててしまうといい。
認識を改めろ。俺たちはより良いPHPerにならないために努力している。
class Request { private $parameters; private $method; function __construct () { $this->method = $_SERVER['REQUEST_METHOD']; if ( strtoupper($this->method) === 'POST' ) { $this->parameters = $_POST; } else { $this->parameters = $_GET; } } function param ($key) { return isset($this->parameters[$key]) ? $this->parameters[$key] : null; } }
これだけでもいい。たったこれだけでもとても便利だ。ここから拡張してGETやPOSTを明示的に取るメソッドとかも作ってみるといい。自分の手を動かすのだ!
例が良くない。こんなのは引数が20個ある関数から、setを20回呼ぶオブジェクトに変わっただけではないか。
そもそもこの20個の引数とはなんなのか。何かのデータ構造なんであれば連想配列にして引数一つとして渡すべきだし、それぞれまったく異なる用途の変数なのであればWindowsプログラミングじゃあるまいし、20個も引数取る時点で設計が間違えている。
何がいいたいか。別に関数でもオブジェクトでもどっちでもいいということだ。
そんなことで悩んでる暇があったら設計を見直せ。
スキあらば自分自身を返せ。スキあらばオブジェクトを返せ。配列はArrayObjectのARRAY_AS_PROPSで返せ。
ひたすらメソッドチェイン。来る日も来る日もメソッドチェイン。とにかくメソッドチェインを使い続けろ。そこに未来はある。
どんなコードも繰り返すな。もし、少しでも同じコードを書いていたなら、それは関数に置き換えてしまえ。
・・・と、いうのはやめなさい。
一見同じように見えた処理でも前後の流れでまったく違うものということが往々にしてある。
まとめ方にも問題があるケースもある。何でもかんでも関数化すると、関数が膨大に増えていく。君は見たことがあるだろうか。common.phpやfunction.phpの恐ろしさを。
確かに細かく関数化はされているが、適切に関数化していないのである。結合度が非常に高い。なんでもかんでも盲目的にまとめれば良いという話ではないのだ!
あまりに極度に意識しすぎると、プログラムそのものができなくなる。そういう状態に陥る。
気を抜いて。そう気を抜いて。所詮あなたのコードなんてすぐに消えてなくなるよ。きっともっと偉い人が作り直すよ。だからまずは思うが侭にやるといい。
結合度を減らすというのは非常に難しい。何度も何度も失敗し続けて、ようやくここは分けた方が良かったんだなと気付く。次に活かそうと心に決める。そしてまた同じ過ちを繰り返していくわけだ。
まずは実装することだ。これが一番の早道だ。まずはがっつり結合した関数をあえて作るといい。何も考えずに作ろう。
そしてその後に、一部分使いまわしたいとおもうことがあるはずだ。その時に関数に切り出そう。それを繰り返すといい。そのうち初めから分けた方が良いと気付く。
何事も経験が必要である!経験を積まないプログラマは丸めてゴミ箱に捨ててしまえ。
さて、先の例で言うならば、私ならadd_result_outputという関数を作ってしまうだろう。だって、addとresultを連続して呼ぶのはめんどくさいんだもん。一連の流れをいつも使うのなら、その流れをやってくれる関数を作ればいいじゃないか。
function add_result_output ($iVar, $iVar2) { $r = add($iVar, $iVar2); echo result($r); }
もっと言えばクラス化してしまってもいいかもしれない。どんな感じになるかは君の手を動かして確認しよう!
このTipsはとてもわかりにくく、ニッチ過ぎる部分も多いかもしれない。
あくまでも「より良いPHPerにならないための20Tips」なのだ。
君はこの記事を鵜呑みにしてはならない。PHPをPHPと見抜けないPHPerはPHPを使うのは難しい。
もし、あなたがPHPプログラマなら、公式のPHPドキュメントはあなたのケツの穴を拭くための紙になるだろう。
私は、それぞれのセクションを眺めて、各関数でどんなことが出来るかなんぞ、歯クソのゴミ程に役に立たないとおもっている。動けばいい。はは。
あなたは、PHPで用意された既製関数で多くのことが実現できることに、(俺の仕事を減らすなと)驚くはずだ。
この記事があなたの役に立たない事を。
ふざけんな!
マジな要素を抽出するとしたら、
1から100までコードをべた書きするのが実行速度的には効率良いのだけど、
それを手で書くのはバカらしいので、
コンパイル時に一種のプログラムを実行してコードを生成しよう、という考え方。(メタプログラミングという)
コンパイル時にFizzBuzzをやってしまっておけば、実行時にはもうしないで済む。
この手のメタプログラミングはC++のtemplate周辺が恐らく発祥の地で、
いまD言語がそれを色々発展させてる。
汎用的なデータ構造(配列やら連想配列やらツリー)やアルゴリズムの実装とか、
普通にやると似たようなコードをたくさん書かなきゃいけない場合に使われる。
前の60行テンプレートエンジンを改良して、レイアウトテンプレート機能を追加してみた(それでも全部で90行)。
レイアウトテンプレート機能とは、例えば個別のテンプレートが<table>...</table>を出力して、それをレイアウトテンプレートが<html><body>...</body></html>で囲って出力するとかそんなの。
詳しくは終わりの方のサンプルをみてくれ。
これは Ruby on Rails(とその仲間たち)にある便利機能のひとつ。
ついでにいうとSmartyにはない機能のひとつ。
今まで知らなかった人はぜひ試してくれ。チョー便利だから。
前回はたくさんのブックマークありがと。
コメントで「男前テンプレート」と名前がついてたので、勝手に採用。
あと、これ以上の機能追加はしないので、各自勝手に改造して使ってくれ(そのためにコメントをつけてるから)。何でも人任せにするな。
コード:
<?php /* * OtokomaeTemplate.php -- レイアウトテンプレートに対応した90行のテンプレートエンジン * * - レイアウトテンプレート中で echo $_content; とすると中身が表示される。 * - テンプレート中で設定した変数をレイアウトテンプレートで使うことが可能。 * - レイアウトテンプレート名をテンプレート側で指定することも可能。 * - 使い方: * require_once('OtokomaeTemplate.php'); * $TEMPLATE_DIR = 'templates'; // 省略可、パーミッションに注意 * $LAYOUT_TEMPLATE = 'layout.php'; // 省略可 * $context = array('title'=>'Example', * 'list'=>array(10,'<A&B>',NULL)); * include_template('template.php', $context); * - 要 PHP 5.1 or later * - ライセンス: public domain (自由に改造してね) */ /* * 設定用のグローバル変数 */ $TEMPLATE_DIR = NULL; /* テンプレートを探すディレクトリ */ $LAYOUT_TEMPLATE = NULL; /* レイアウトテンプレートのファイル名 */ /* * テンプレートを読み込んで実行する。 * $_context は変数名をキー、値を要素とする連想配列。 * $_layout はレイアウトテンプレートのファイル名。 * - NULL または省略した場合は $LAYOUT_TEMPLATE を使う。 * - FALSE ならレイアウトテンプレートを使わない。 * - $_context['_layout'] = '...'; とすればテンプレート側でも指定可能。 */ function include_template($_filename, $_context, $_layout=NULL) { global $LAYOUT_TEMPLATE; $_content = render_template($_filename, $_context); if (@$_context['_layout'] !== NULL) // テンプレート側で指定された場合は $_layout = $_context['_layout']; // それを使う。 elseif ($_layout === NULL) // 引数で指定されなかった場合は $_layout = $LAYOUT_TEMPLATE; // デフォルトのファイル名を使う。 if ($_layout) { $_context['_content'] = $_content; // レイアウトテンプレート中で使う変数 $_content = render_template($_layout, $_context); } echo $_content; // or return $_content; } /* * テンプレートを読み込んで実行し、その結果を文字列で返す。 * include_template() の実体。 */ function render_template($_filename, &$_context) { $_cachename = convert_template($_filename); extract($_context); // 連想配列をローカル変数に展開 ob_start(); include($_cachename); // テンプレートを読み込んで実行 return ob_get_clean(); } /* * テンプレートファイルを読み込み、convert_string() で置換してから * キャッシュファイルに書き込む。読み込み時のロックは省略。 * (file_get_contents() もファイルロックできるようにしてほしいなあ。) */ function convert_template($filename) { global $TEMPLATE_DIR; if (! file_exists($filename) && $TEMPLATE_DIR) $filename = "$TEMPLATE_DIR/$filename"; $cachename = $filename . '.cache'; if (! file_exists($cachename) || filemtime($cachename) < filemtime($filename)) { $s = file_get_contents($filename); $s = convert_string($s); file_put_contents($cachename, $s, LOCK_EX); // LOCK_EX サポートは 5.1.0 から } return $cachename; } /* * テンプレートの中身を置換する。 * - '#{...}' を 'echo ...;' に置換 * - '%{...}' を 'echo htmlspecialchars(...);' に置換 * - ついでにXML宣言も置換 */ function convert_string($s) { $s = preg_replace('/^<\?xml/', '<<?php ?>?xml', $s); $s = preg_replace('/#\{(.*?)\}/', '<?php echo $1; ?>', $s); $s = preg_replace('/%\{(.*?)\}/', '<?php echo htmlspecialchars($1); ?>', $s); return $s; } ?>
<?php require_once('OtokomaeTemplate.php'); $TEMPLATE_DIR = 'templates'; $LAYOUT_TEMPLATE = 'layout.php'; $context = array('list'=>array(10,'<A&B>',NULL)); include_template('template.php', $context); ?>
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> <body> <h1>%{$title}</h1> <div id="maincontent"> <!-- テンプレートの内容 --> <?php echo $_content; ?> <!-- /テンプレートの内容 --> </div> </body> </html>
<?php // レイアウトテンプレート名をテンプレート中で指定する場合 ?> <?php //$_context['_layout'] = 'mylayout.php'; ?> <?php // レイアウトで使用する変数をテンプレート中で指定する場合 ?> <?php $_context['title'] = 'レイアウトのサンプル'; ?> <table> <?php foreach ($list as $i=>$item): ?> <tr bgcolor="#{$i % 2 ? '#FFCCCC' : '#CCCCFF'}"> <td>#{$i}</td> <td>%{$item}</td> </tr> <?php endforeach ?> </table>
出力例:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> <body> <h1>レイアウトのサンプル</h1> <div id="maincontent"> <!-- テンプレートの内容 --> <table> <tr bgcolor="#CCCCFF"> <td>0</td> <td>10</td> </tr> <tr bgcolor="#FFCCCC"> <td>1</td> <td><A&B></td> </tr> <tr bgcolor="#CCCCFF"> <td>2</td> <td></td> </tr> </table> <!-- /テンプレートの内容 --> </div> </body> </html>
いくつか補足:
方針:
【追記】レイアウト機能を追加してみた
コード:
<?php /* * SixtyLinesTemplate.php - 60行しかないけどSmartyより速いテンプレートエンジン * * 使い方: * require_once('SixtyLinesTemplate.php'); * $TEMPLATE_DIR = 'templates'; // 省略可、パーミッションに注意 * $context = array('title'=>'Example', * 'list'=>array(10,'<A&B>',NULL)); * include_template('template.php', $context); * * ライセンス: public domain (自由に改造してね) */ /* * テンプレートを探すディレクトリ。 */ $TEMPLATE_DIR = NULL; /* * テンプレートを読み込んで実行する。 * $_context は変数名をキー、値を要素とする連想配列。 */ function include_template($_filename, $_context) { $_cachename = convert_template($_filename); extract($_context); include($_cachename); } /* * filename を読み込み、convert_string() で置換してから * filename.cache に書き込む。読み書きのロックは省略。 * (file_{get,put}_contents() はファイルロックできるようにすべきだ。) */ function convert_template($filename) { global $TEMPLATE_DIR; if (! file_exists($filename) && $TEMPLATE_DIR) $filename = "$TEMPLATE_DIR/$filename"; $cachename = $filename . '.cache'; if (! file_exists($cachename) || filemtime($cachename) < filemtime($filename)) { $s = file_get_contents($filename); $s = convert_string($s); file_put_contents($cachename, $s); } return $cachename; } /* * テンプレートの中身を置換する。 * - '#{...}' を 'echo ...;' に置換 * - '%{...}' を 'echo htmlspecialchars(...);' に置換 * - ついでにXML宣言も置換 */ function convert_string($s) { $s = preg_replace('/^<\?xml/', '<<?php ?>?xml', $s); $s = preg_replace('/#\{(.*?)\}/', '<?php echo $1; ?>', $s); $s = preg_replace('/%\{(.*?)\}/', '<?php echo htmlspecialchars($1); ?>', $s); return $s; } ?>
<?php require_once('SixtyLinesTemplate.php'); $TEMPLATE_DIR = 'templates'; // optional $context = array('title'=>'Example', 'list'=>array(10,'<A&B>',NULL)); include_template('template.php', $context); ?>
サンプルテンプレート:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> <head> <title>%{$title}</title> </head> <body> <h1>%{$title}</h1> <table> <?php foreach ($list as $i=>$item): ?> <tr bgcolor="#{$i % 2 ? '#FFCCCC' : '#CCCCFF'}"> <td>#{$i}</td> <td>%{$item}</td> </tr> <?php endforeach ?> </table> </body> </html>
出力例:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> <head> <title>Example</title> </head> <body> <h1>Example</h1> <table> <tr bgcolor="#CCCCFF"> <td>0</td> <td>10</td> </tr> <tr bgcolor="#FFCCCC"> <td>1</td> <td><A&B></td> </tr> <tr bgcolor="#CCCCFF"> <td>2</td> <td></td> </tr> </table> </body> </html>
本講座ではほかの言語は触ったことがあるが、Ruby未経験者を対象にRailsを触るための最小限のrubyの知識を身につけるためのものである。
と、堅苦しく書いたけど仕事で急いでrailsを触れうようになるための最低限の情報だけをまとめてみたよ。
これを読んだだけではRubyでプログラムを書けるようにはならないと思うけど、Railsを使った開発だといきなり何もできないってことはなくなるはずだよ。
肩の力を抜いて読んでほしいな。
いきなり今まで宣言していない名前を使ってもおこらえないよ。
answer_to_life_the_universe_and_everything = 42 # answer_to_life_the_universe_and_everything ????~A~S??~S????~H~]??~A????~Y??| ??~A~Y??~K??~B
なんて書いても問題ないんだ。
静的言語をやってきた人には型の宣言がないことに違和感を感じる人もいるかもしれないけど、Rubyでは型はあんまり気にしないんだ。
int x = HogeHoge.new;
みたいな書き方では
変数xを用意してそこにHogeHoge.newを代入する。といった説明を受けたと思う。
でもRubyではちょっと違う考え方をするんだ。
x = Foo.new
だとFoo.newの変数を用意して、それを名前に"束縛"するって説明することが多いよ。
ラベルが貼られた箱を用意してそこに何かを入れるか、何かが入った箱を用意してそれにラベルを張るかの違いだね。後者のほうがちょっとだけ柔軟なんだ。
そして、柔らかいほうがRuby流って気がするよ。
でもそんなRubyでもちょっとだけ名前に関するルールがある。
小文字からはじまる名前が局所変数
ってルールだよ。
Railsだったらとりあえず@からから始まる名前にしておけばたいていは問題ないんじゃないのかな。
JavaやC言語ではif文なんかをよく使うよね。Rubyでもほとんど一緒だよ。
if something_condition do_some_thing elsif other_condition do_another_thing else do_something_if_you_wont end
もちろんelsifやelseが必要なければ省略することができるよ。
ほかにもRubyには便利な分岐の書き方があるよ。
so_some_thing if some_condition
みたいに後置でも書けるんだ。
どっちでも好きなほうを使えばいいと思うよ
def func_name ( some_val_names ) do_some_thing end
といった感じでdef..endでくくるだけで簡単に関数を宣言できるよ
そして関数の戻り値は最後に評価した値が自動的に戻り値になるんだ。
def baz 10 end
これは常に10を返す関数の例だよ。
プログラムをしていて非常によく使うデータの型といえば配列(Array)や連想配列(Hash)だよね。
Rubyではそれらを簡潔に使うために専用の文法を用意している。
それが[]と{}だ。
arr = [1,2,3,"A","B","CDE"] #??~M很~W宣訾@ has = {1 => "A",2 => "B", 3 => "C"} #??~O??~C????~CťΣ訾@ puts arr[3] puts has[2]
A B
javascriptで存在するほとんどのオブジェクトの実体はハッシュだよ。
var arr = [0,1,2,3];
とかをみると配列(人によってはリスト)に見えると思う。でも実際は違うんだ。
これは
var has = {0:0,1:1,2:2,3:3};
と基本的には等価なんだ。ただちょっと束縛されているメソッド(インターフェイス)が違うだけ。
ためしに
arr[4] = 4; arr['x'] = 'string'; arr[-1] = -1;
としてみよう。
Firebugで確認してみると[0, 1, 2, undefined, 4]というような値がかえってくるよ。
でもarr[-1]やarr['x']の値は保存されてないのかな?そんなことはないちゃんとアクセスできるんだ。
それどころかarr.xで'string'がかえってくるんだ。
別の例を見てみよう。
var fx = function(){}; fx[0] = 'somestring';
こうするとfx[0]に'somestring'が束縛される。
つまりfunctionも一つのハッシュだったんだ。
これでほとんどのものがハッシュだということが解ってくれたかな?
ハッシュじゃないのは文字列とか数字とかそいういシンプルなものだけなんだ。
ハッシュへはhash[name]でアクセスすることが出来る。
それ以外にもnameが演算子や空白を含まなくて頭が数字でない場合はhash.nameでアクセスできるんだ。
これでhash[name]が関数だったらhash.name(args)とできるよ。まるでメソッドみたいだね。
関数には名前を付けなくても使用可能だよ。
function(x){return x+x}(2); -> 4
関数宣言の書き方って次見たいの覚えてる人が多いんじゃないかな?
function funcname(args){ do something};
でもこれはsystax sugerだってことを知ってほしい。
上でも使ってるんだけど。
var funcname = function(args){ do something};
と等価になる。もちろんどちらの書き方でもかまわないよ。
ただ、(無名)関数を宣言してそれをfuncnameに束縛しているっていうことを理解しておくと便利だよ。
javascriptはレキシカルスコープを採用してるんだ。
レキシカルスコープなんていうと難しく感じるかもしれないけど、結構単純なんだ。
var x = 'global'; var fx1 = function(){return x}; var fx2 = function(){var x = 'local';return x}
これの実行結果は以下になるよ。
fx1() -> 'global' fx2() -> 'local' fx1() -> 'global'
fx2の変数xはほかの場所に影響しないんだ。これは関数の中と外ではスコープが違うからなんだ。
でも以下のような場合に注意してほしいな。
var x = 'global'; var fx1 = function(){return x}; var fx2 = function(){x = 'local';return x}
fx1() -> 'global' fx2() -> 'local' fx1() -> 'local'
fx2は自分のスコープに変数xがないからその外側スコープに探しに出かけたんだ。
つまり宣言文varはそのスコープに新しい名前を登場させる機能なんだよ。
別の場合を見てみようね。
var x = 'global'; var fx1 = function(){ var x = 'local'; return function(){return x} }; var fx2 = fx1();
x -> 'global' fx2() -> 'local'
この例だとfx2()の値がlocalになってるね。
なぜなら外側スコープっていうのは関数を評価した場所じゃなくて、関数を定義した場所の外側なんだ。
一つ上の例では実際に関数を返り値にしているね。
var fx1 = function(){ var x = 0; return function(){ x = x+1; return x; } }; var fx2 = fx1(); var fx3 = fx1();
fx2() -> 1 fx2() -> 2 fx2() -> 3 fx3() -> 1 fx3() -> 2
fx2とfx3の値が違うのはそれぞれ別の関数が作られるからだよ。
こんな風に関数が状態を持つことも出来るんだ。クロージャーとよんだりするよ。
関数がハッシュを使って複数の関数を返すとこんなことも出来るよ。
var fx = function(){ var x = 0; return { 'up':function(){ x = x+1; return x; }, 'down':function(){ x = x-1; return x; } } }; var obj = fx();
obj.up(); -> 1 obj.up(); -> 2 obj.down(); -> 1 obj.down(); -> 0
thisという機能があるよう。ちょっとだけつかってみるね。
var x = 0; var add = function(n){this.x = this.x + n; return this.x}; var mul = function(n){this.x = this.x * n; return this.x}; var obj = {'x':0,'do':add};
add(1); -> 1 add(1); -> 2 mul(2); -> 4 obj.do(); -> 1 obj.do(); -> 2 obj.do = mul; obj.do(); -> 4
thisは評価された場所によって値がかわるよ。
objの中にいるときはobj.xを扱うし、グローバルにいるときはグローバルのxを扱うんだ。
http://anond.hatelabo.jp/20070620200618を改訂してみたよ。
このぶんしょうがjavascriptを覚える上の一助になるとうれしいんだ。
あとここでつかってるハッシュは伝統的な意味。連想配列のことね。
突っ込みも大歓迎だよ。
'/** Requestオブジェクトから受信したデータを取り出します。 ' * @return byte配列を格納した連想配列を返します。 ' */ Function getRequestItem() If Request.TotalBytes <= 0 Then getRequestItem = Null Exit Function End If Dim data Dim separator Dim dataArray Dim buffer Dim filePath Dim fileName Dim ix Dim stringIndex Dim myCrLf Dim items Dim itemName myCrLf = convertAsc(vbCrLf) Set items = Server.CreateObject("Scripting.Dictionary") data = Request.BinaryRead(Request.TotalBytes) data = LeftB(data, UBound(data) - 3) & myCrLf separator = MidB(data, 1, InStrB(1, data, myCrLf) + 1) dataArray = SplitB(data, separator) For ix = 2 To UBound(dataArray) Step 1 '1行読み込み buffer = MidB(dataArray(ix), 1, InStrB(1, dataArray(ix), myCrLf) - 1) 'アイテム名の取得 stringIndex = InStrB(1, buffer, convertAsc("name=")) If stringIndex > 0 Then itemName = convertUnicode(MidB(buffer, stringIndex + 6, InStrB(stringIndex, buffer, ChrB(34)) - stringIndex)) Else itemName = "" End If 'ファイル名の取得 stringIndex = InStrB(1, buffer, convertAsc("filename=")) If stringIndex > 0 Then filePath = MidB(buffer, stringIndex + 10, InStrB(stringIndex, buffer, ChrB(34)) - stringIndex) fileName = RightB(filePath, LenB(filePath) - LastInStrB(0, filePath, convertAsc("\"))) Else fileName = "" End If 'データ部の取得、改行コードの切り捨て buffer = RightB(dataArray(ix), LenB(dataArray(ix)) - InStrB(1, dataArray(ix), myCrLf & myCrLf) - 3) buffer = LeftB(buffer, LenB(buffer) - 2) items.Item(itemName) = parseBytes(buffer) Next Set getRequestItem = items Set items = Nothing End Function '/** 文字列の最後尾から指定文字を検索します。 ' * @param Start 検索する開始文字位置を指定します。 ' * @param String1 検索対象の文字列を指定します。 ' * @param String2 検索する文字列を指定します。 ' */ Function LastInStrB(ByRef Start, ByRef String1, ByRef String2) Dim ix Dim lastIndex Dim searchLength searchLength = LenB(String2) lastIndex = LenB(String1) - searchLength + 1 If Start > 0 And Start < lastIndex Then lastIndex = Start End If For ix = lastIndex To 1 Step -1 If MidB(String1, ix, searchLength) = String2 Then LastInStrB = ix Exit Function End If Next LastInStrB = 0 End Function '/** アスキー文字列に変換します。 ' * @param String 対象の文字列を指定します。 ' */ Function convertAsc(Byref String) Dim ix Dim ascii ascii = "" For ix = 1 To Len(String) Step 1 ascii = ascii & ChrB(AscB(Mid(String, ix, 1))) Next convertAsc = ascii End Function '/** Unicode文字列に変換します。 ' * @param AscString 対象のアスキー文字列を指定します。 ' */ Function convertUnicode(Byref AscString) Dim ix Dim unicode unicode = "" For ix = 1 To LenB(AscString) Step 1 unicode = unicode & Chr(AscB(MidB(AscString, ix, 1))) Next convertUnicode = unicode End Function '/** バイナリ対応版Split関数です。 ' * @param String 対象の文字列を指定します。 ' * @param Delimiter 区切り文字を指定します。 ' */ Function SplitB(Byref String, Byref Delimiter) Dim ix Dim lastIndex Dim searchLength Dim start Dim datas() Dim dataIndex dataIndex = 1 start = 1 delimiterLength = LenB(Delimiter) lastIndex = LenB(String) - delimiterLength + 1 '最初から1文字ずつ繰り返します。 For ix = 1 To lastIndex Step 1 'データを比較します。 If MidB(String, ix, delimiterLength) = Delimiter Then 'データを取り出せたら配列に格納します。 ReDim Preserve datas(dataIndex) datas(dataIndex) = MidB(String, start, ix - start) dataIndex = dataIndex + 1 start = ix + delimiterLength End If Next SplitB = datas End Function '/** Byte配列を返す関数です。 ' * @param data 対象のデータを指定します。 ' */ Function parseBytes(Byref data) Const adLongVarBinary = 205 Dim recordset If LenB(data) <= 0 Then parseBytes = CByte(0) Exit Function End If Set recordset = Server.CreateObject("ADODB.Recordset") recordset.Fields.Append "UpLoadBinary", adLongVarBinary, LenB(data) recordset.Open recordset.AddNew recordset.Fields("UpLoadBinary").AppendChunk data recordset.Update parseBytes = recordset.Fields("UpLoadBinary").Value recordset.Close Set recordset = Nothing End Function