仕事の都合で東京のはずれに引っ越しました。
新潟にいる間は、新潟のプログラマがとても仲良くしてくれて、おかげでプログラマとしてめちゃめちゃ楽しく生活することができました。なかでも @hayajo さん、@civicさん、@dictavさんには格別にお世話になりました。ズッ友だょ!!
東京のプログラマのみなさん、今後ともよろしくおねがいします。
wishlistはこちらです。
まずはこちらをごらんください。
すごすぎる……。恐ろしいですね。
なぜこんなことになるのか、解説していきましょう。まずはPerlの気持ちになりましょう。
件のプログラムは、base64 っぽい文字列が書かれていますが、これを前からPerlコードとして読んでいくと、大きく2つのパートに分かれることに気づきます。というのも、前から一文字ずつ読んでいくと、「+」という演算子にぶつかるわけですね。
それに気づくと、このコードは前半部分
dXNlIE1JTUU6OkJhc2U2NDtwcmludCBlbmNvZGVfYmFzZTY0IGpvaW4nJyw8PjsKX19FTkRfXwo
と、
s//v62/e+s//v60/e+s//v44/e+s//v39/e+s//v39/e+s//join/+s//v32/e+s//base64/ss+
s//v95/e+s//decode/+s//v32/e+s//print/+s//v59/e+s//Base64/+s//v58/e+s//v58/e
+s//MIME/+s//v32/e+s//use/s/eval
に分けることができる、と気づくでしょう。
まずは前半部分からやっつけていきましょう。Perlコードにおいて、""
などで囲まれていない上にsigilを持たない文字列は、「bare word」として解釈され、use strict
をしていない環境では、同名の解決可能な関数などが見つからない場合、文字列扱いになります。
my $bare = nyan; print $bare; # => nyan
というわけで、前半部分はdXNlIE1JTUU6OkJhc2U2NDtwcmludCBlbmNvZGVfYmFzZTY0IGpvaW4nJyw8PjsKX19FTkRfXwo
というbarewordとして解釈され、この部分は"dXNlIE1JTUU6OkJhc2U2NDtwcmludCBlbmNvZGVfYmFzZTY0IGpvaW4nJyw8PjsKX19FTkRfXwo"
という文字列として解釈されます。
一旦文字列として解釈された"dXNlIE1JTUU6OkJhc2U2NDtwcmludCBlbmNvZGVfYmFzZTY0IGpvaW4nJyw8PjsKX19FTkRfXwo"
は、 +
オペレータに渡されます。このとき、+演算は渡されたものを数値コンテキストとして解釈します。文字列"dXNlIE1JTUU6OkJhc2U2NDtwcmludCBlbmNvZGVfYmFzZTY0IGpvaW4nJyw8PjsKX19FTkRfXwo"を数値コンテキストとして解釈するとどうなるでしょうか。やってみましょう。
✔ perl -e 'print "dXNlIE1JTUU6OkJhc2U2NDtwcmludCBlbmNvZGVfYmFzZTY0IGpvaW4nJyw8PjsKX19FTkRfXwo" + 0' 0
parseできないので、0として解釈されているのがわかるでしょう。
これで、前半をPerlコードとして見たときには 単に '0 + 後半'というコードとして解釈されることがわかりました。では後半にいきましょう。
さて、同じように読み進めていくと、'+'という記号がキモになっていることに気づくでしょう。前から順に + で様々な項が前から順に評価されています。
後半を仔細に見ていくと、基本的に s//vなんか/e
という項と、 s//なんか/sが0からn個
という項が+
で連結されていて、最後にs//use/s/eval
が続いていますね。ではそれぞれの項がどう評価されるか見てみましょう。
s/なんか/べつのなんか/option
というのは、正規表現の置換リテラルですね。ではeというオプションはなにかというと、s/なんか/べつのなんか/e
の、「べつのなんか」の部分をPerlコードとして評価する、というオプションです。では「vなんか」というbarewordはPerlにとってどういう意味を持っているでしょうか。これは「ヴァージョン文字列」と呼ばれるものです。(see perldata - Perl のデータ型 - perldoc.jp )。
というわけで、v62
の評価結果は、10進数における62を16進数に変換して、"\x3e"となります。これは">"です。
ところで、s//v62d/e
の部分、=~
でマッチしていませんね。この場合、Perlは$_
というデフォルト変数に対する置換として解釈します。今$_は空ですから、空文字列に対して、「空文字列にマッチしたら最初の空文字列を">"として置き換えてそれを$_に代入する」という動きをします。つまりこういうことです。
s//v62/e; print $_; # => ">"
要するに、$_の先頭にv62dの評価結果を文字列コンテキストで挿入してるわけですね。そして、これを繰り返すことで、$_
に文字列を貯めていきます。
これは「0からn文字のs」が置換正規表現のオプションとして扱われますね。ではsというオプションは何を意味するでしょうか。これは「ワイルドカードのドット( . )が改行にもマッチするようにする」というオプションです。今回は改行使ってないので、この「0からn文字のs」については無視して良いことになりますね。
さて、そうなると、s//なんか/0からn文字のs
の評価結果は、$_ の先頭に「なんか」の部分の文字列を貯めていくことになるわけです。
さて、最後はs//use/s/eval
という項です。
これはちょっとトリッキーですが、Perlの気持ちになって読むと、(今まで解釈してきた部分 + s//use/s) / (eval)
という割り算として解釈できます。 /
の優先順位は +
より高いですからね。 つまり、今までは単純に前から読んできたけど、評価順としては、(今まで読んできた部分 + s//use/s) を評価して、そのあと(eval) を評価して、最後に / で割り算する、という形ですね。でも割り算の結果は捨てられてるので、結果的には前から順に評価されてるのと同じことです。
では最後の部分をみてみましょう。s//use/s
についてはさきほど見たとおりですね。 $_ の先頭に "use" をappendするように解釈されます。そして、そのあと(eval) が評価されるわけですが、evalは引数が省略された場合、$_を暗黙の引数として取ります。これで、「最後の項を除く部分を評価した結果得られた $_ をevalで評価する」というプログラムの完成です。
では、今まで見てきたものをまとめましょう。
dXNlIE1JTUU6OkJhc2U2NDtwcmludCBlbmNvZGVfYmFzZTY0IGpvaW4nJyw8PjsKX19FTkRfXwo+ s//v62/e+s//v60/e+s//v44/e+s//v39/e+s//v39/e+s//join/+s//v32/e+s//base64/ss+ s//v95/e+s//decode/+s//v32/e+s//print/+s//v59/e+s//Base64/+s//v58/e+s//v58/e +s//MIME/+s//v32/e+s//use/s/eval
(dXNlIE1JTUU6OkJhc2U2NDtwcmludCBlbmNvZGVfYmFzZTY0IGpvaW4nJyw8PjsKX19FTkRfXwo+ s//v62/e+s//v60/e+s//v44/e+s//v39/e+s//v39/e+s//join/+s//v32/e+s//base64/ss+ s//v95/e+s//decode/+s//v32/e+s//print/+s//v59/e+s//Base64/+s//v58/e+s//v58/e +s//MIME/+s//v32/e+s//use/s) / (eval)
を評価した結果、
$_ = "use MIME::Base64;print decode_base64 join'',<>"; eval $_;
という計算が得られることになります。おお!!!base64デコーダだ!!!
では、Perlの気持ちになったときに無視されていた前半のbarewordをbase64として解釈してみましょう。これはbase64エンコードするPerlコードになっています。
echo "dXNlIE1JTUU6OkJhc2U2NDtwcmludCBlbmNvZGVfYmFzZTY0IGpvaW4nJyw8PjsKX19FTkRfXwo+" | base64 -D use MIME::Base64;print encode_base64 join'',<>; __END__
この出力をPerlコードとして解釈すると、__END__以下は無視されるから、そのあとは(base64としてvalidならば)自由になんでも書くことが可能!!!!!!結果、base64エンコードするPerlコードが得られるわけですね。かしこーーーーーーーい!!!!
Perlの黒魔術を説明しつつ、黒魔術コードを読み解いてみました。読む方としては「なるほどなー」って感じだけど、これを書くの変態すぎるしすごすぎる……。これ、base64が'+'と'/'を使えることと、Perlの'$_'と、ヴァージョン文字列とかいう変態仕様を最大限悪用してるんですよ。すごすぎる!!!という思いを得ることになりましたね!!!!世の中どんだけ天才がいるんだよ……。
プログラミング技術の歴史は、ありとあらゆる歴史がそうであるように、いろんな「史観」で眺めることができます。ならば、プログラミング技術の歴史を、「エラーハンドリングとの戦い」という視点から見ることもできるのではないでしょうか。本日は、エラーハンドリングとの戦いの歴史を俯瞰することで、エラーハンドリングの勘所について考えていこうと思います。
なお、このエントリはNDSという勉強会の第41回で発表した内容と同一です。
Cの時代のエラーハンドリングでは、関数の返り値と、グローバル変数errnoを見ることで処理が成功したか失敗したかを見るのが一般的でした。
例として、文字列をlongに変換するstrtol関数をmanで引いてみましょう。すると、だいたい以下のようなことが書かれています。
さて、一見してとてもシンプルな仕様ですが、現代的な視点からみてみると、いくつか気になることがありそうです。ざっと挙げてみると……
int main(int argc, char *argv[]){ long l = strtol("string"); // なんかこうごちゃごちゃといろいろやる some_function(l); // ここでおかしなことになる return 0; }
これは結構怖いですね。たとえば"String"みたいな文字列をstrtolに渡して、エラーチェックをしなかった場合、0という数だと解釈されたまま、プログラムは動き続けます。そして、どこかこのstrtolを呼び出したところから遠く離れたところで、いきなりプログラムがクラッシュします。バグを入れ込んでしまったところと、バグが発見されたところが遠く離れているとき、デバッグは困難を極めます。うーん。おそろしい。
では、必ずエラーを手動でチェックすればそれで問題は解決でしょうか。そんなことはありません。たとえばあなたが文字列と文字列を受け取り、それをlongとして解釈して足す関数を含むライブラリ作成しているとしましょう。このとき、もしstrtolにinvalidな文字列を渡してEINTVALが帰ってきても、自分ではそのエラーをどうハンドリングすべきか、ということはわかりません。なぜかというと、「変な値入れられた時にそれをどう扱うべきか」というのは、ライブラリが決めるべきことではなくて、アプリケーションの要件によって決まることだからです。
なので、あなたはあなたが作成している関数の利用者に対して「invalidな文字列が渡されたよ」というエラーを通知する必要があるわけです。つまり、エラーを上流に伝播させる必要があるわけです。そして、このようなシチュエーションは決して珍しいものではありません。そのたびにあなたは起こりうるエラーに対してすべてエラーコードを定義して、エラーの場合の返り値を決めて、という作業をしなければいけません。そして、あなたの書くコードは本質的な処理よりもエラーハンドリングのためのコードによってどんどん太っていきます。
つまり、このやり方では、
という問題があるわけです。
そして、それだけではありません。まだ気になる点はあります。それは、
という点です。グローバル変数は、いつ書きかわるかわかりません。なので、errnoをあとから参照したい場合はそれ用の変数を作ってコピーしておく、という回避策が一般に取られています。
int main(int argc, char *argv[]){ long l = strtol("string"); int strtol_err = errno; // コピーしとかないといけない // ちょっとなんかやる間にerrnoが書きかわる可能性‥‥ if (strtol_err== EINVAL) { // エラー処理 } else if (strtol_err == ERANGE) { // エラー処理 } return 0; }
シングルスレッドで動いているならば、「次の行で必ずチェック、あるいはコピーする」ということを徹底すれば(それだって結局人間がやらなければならないのですけれど)問題にはならないでしょう。しかし、もしもこれがマルチスレッドで動いていたら?errnoは本当に「どのタイミングで書きかわるかわからない」ものになります。
さらにもうひとつ問題があります。それは「エラー時にリソースの解放をするのが煩雑」という点です。
エラーになってしまったとき、mallocで確保していたものをfreeせずに早期returnなどをすると、正常系では解放されるリソースが解放されなかったりします。これを防ぐためによるあるパターンは、関数の後ろのほうにリソース解放の処理を書いておき、返り値は変数に入れておき、gotoでリソース解放のところにすっ飛ぶパターンです。
int nyan(){ int *p1 = (int *)malloc(sizeof(int)) int *p2 = (int *)malloc(sizeof(int)) int *p3 = (int *)malloc(sizeof(int)) int retval = 0; // ごちゃごちゃなんかやる //!エラーが起こった! if (err) { retval = -1; // エラーコードを入れて goto cleanup; //cleanupにすっ飛ぶ } //正常系は続く retval = 1; //正常系の場合の返り値を入れて cleanup: free(p1); free(p2); free(p3); return retval; }
エラー処理周りのコードだけで、こんなに大きな関数になってしまいました。バグを入れ込みそうで怖いですね!一時期話題になったApple史上最大のセキュリティバグ、goto fail; なんかはこのパターンをつかってたやつです。
Cスタイルのエラーハンドリングの問題点については、おおまかにこんなところでしょうか。
一度まとめておくと、
ですね。
さて、Cの時代のような問題に対抗するために、人間は例外という新しい武器を作り出しました。例をあげながら、上述の問題点がいかに解決されているのかを見てみましょう。
例外をrescue(キャッチ)しわすれるということはあり得ますが、その場合もプログラムはすぐさまクラッシュしてくれるので、エラーを無視してしまっても、「間違えた内部状態のままプログラムが進んでしまって、バグを入れ込んだところと遠く離れたところでいきなりクラッシュする」というようなことは防げるようになりました。
class WanError < StandardError; end def wan raise WanError, "エラーだよ!!!" end wan # rescueしていないのでここでプログラムは止まってしまう
例外をraiseしてそれをrescueしなかった場合、例外はコールスタックを上流に向かってどんどん突き進んでいきます。なので、何も書かなくても nyan の中でよんだ wan のエラーを main で捕まえることができます。
class WanError < StandardError; end def nyan wan end def wan raise WanError, "エラーだよ!!!" end begin nyan rescue WanError => e # wanの中で発生したエラーをここで捉えられる p e end
例外が起こるたびに例外オブジェクトを発生させるので、グローバル変数にエラーを入れておく必要がありません。
ensureやfinaly(例外が起こっても起こらなくてもかならず実行される部分)があるので、そこでお片付けすればシンプルです。
r = Resource.new begin # do something rescue => e # do something ensure r.close end
さて、いいことづくめであるような気がする例外機構ですが、近年、この例外機構をもってしても解決できない問題が人類を襲いました。
例外をキャッチしわすれると、アプリは死にます。それはもう見事に簡単に死にます。Javaの検査例外は例外のキャッチし忘れをコンパイル時に見つけてくれる仕組みですが、批判も多い機能ですね。今回はちょっと分量的に無理なので検査例外の話には立ち入りません。
一般に、非同期処理が絡むと、例外の扱いはかなり難しくなってきます。というのも、(たとえば)スレッドAで起こった例外は、そのままスレッドBでキャッチすることはできません。これは、スレッドAとスレッドBが別のコールスタックを持っていることを考えれば当然のことです。そのため、スレッドをまたいだ例外の取り扱いというのは非常にむずかしいものとなります。Javaはそれに対して Callable と Future という回答を出し、それは一定の成果を上げていると言えそうです(このあたりも詳しく入り込む余裕がないので入り込みません)。しかし、たとえば goroutine のように、非同期なタスクから連続的に値を受け取りたいときなどはどうすればいいでしょうか? Runnable では依然として別スレッドの例外を補足する方法はなく、非同期処理と例外機構というのは、やはり結構相性が悪いもののようです。
さて、Cスタイルのエラーハンドリングに対して、例外とは別の方向から回答を出したのが、関数型界隈でよく使われているEitherというデータ型です。
Scalaの例で説明しましょう。Scalaにおける Either というのは、LeftかRightどちらかの値を持つデータ型です。LeftとRightはコンテナになっていて、どんな値でもその中に入れることができます。
Eitherの使い方としては、正常に処理が成功した場合はRight(正しい、という意味のRightと掛けている)に値を突っ込んで、失敗した場合は失敗の理由などを表すオブジェクトをLeftに突っ込んで返します。こんな感じ。
def divide(x:Int, y:Int): Either[String, Int] = { if (y == 0) { Left("can't divide by zero") } else { Right(x / y) } } divide(2, 2) // => Right(1) divide(0, 0) // => Left("can't divide by zero")
Cスタイルと同じく、値としてエラーかどうかを返すスタイルです。が、Eitherの場合どのようにCスタイルの問題が解決されているのか見てみましょう。まず、
という点は解決されています。というのも、Eitherの中身はそのままでは使えません。なんらかの方法で取り出す必要があります。
たとえば、Eitherの中身は、パターンマッチで取り出すことができます。
val either = divide(2, 2) either match { case Right(x) => println(x) case Left(message) => println(message) }
このとき、RightだけでパターンマッチしたりLeftだけでパターンマッチしようとすると、コンパイラが「caseが網羅的じゃないよ」と怒ってくれます。終始こんな感じで、Leftを無視してRightの中身だけを扱おうとするとコンパイラに怒られる仕組みが揃っています。
次に
という問題について見てみましょう。Eitherにはrightというメソッドがあって、これを呼ぶとRightProjectionというものが取得できます。これは「Eitherのright側を正当なものとして扱うよ」と決めたもの、のようなものです。このRightProjectionにはmapメソッドが生えていて、そのmapメソッドは「Leftの場合はそのままLeftを返して、Rightの場合は引数に指定した計算を行う」という挙動をします。
def divideAndDouble(x: Int, y:Int): Either[String, Int] = divide(x, y).right.map(_ * 2) divideAndDouble(2, 2) // => Right(2) divideAndDouble(0, 0) // => Left("can't divide by zero")
このように、map(やflatMap)を利用することで簡単にエラーを伝播させることができます(for式やモナドについては触れません。興味があれば調べてください)。
毎回Eitherを作るのでグローバル変数は駆逐できます。
これはEitherによって解決されるものではないですが、関数型スタイルではリソースの確保や解放は副作用とみなします。関数型スタイルでは、副作用をなるべく局所的にまとめて、ロジックから分離するという別の方法で解決しています(雑な説明ですがここに入り込むとまた時間がかかるのでこれも詳しく気になるひとは調べてください)。
というわけで、Cスタイルの問題点はどうやらEitherでだいぶ解決できそうです。
ここからさらに、例外が持ち込んでしまった問題点をEitherがどのように解決しているかも見てみましょう。
上述の通り、エラーを無視しようとすると、コンパイラが怒ってくれますし、検査例外ほど煩雑でもありません。
例外機構は制御構文ですが、Eitherは単なるデータ型です。スタックを飛び越えたりしないし、「普通の値」として扱えます。なので、例外機構よりも素直に非同期処理においてエラーを扱うことができます。何度も言いますが、単なる値ですから。
golangも、例外機構以外の方法でエラーを扱う言語です。これは、goroutineの存在が大きいのではないでしょうか。goroutineはJavaのFutureとかと異なり、goroutine同士でデータをやりとりするために channel を使います。このとき、素直な例外機構はまったく役に立ちませんよね。
そこでgolangは、返り値を複数持てることを利用して、最初の返り値に正常系の返り値、2つめの返り値に異常系の時のエラー値を返す、という「慣習」をつくることにしてしました!!!!
file, err := os.Open(filename)
これはかなり大胆な考えかたですが、実は結構バランスのとれた解だと思います。
まず、うっかりerrを受け取り忘れると、コンパイル時に怒られます
f := os.Open(filename) //multiple-value os.Open() in single-value context
さらに、errを受け取ったとして、それを無視してもコンパイル時に怒られます
f, err := os.Open(filename)
// このあとなにもしないと、err declared and not usedと怒られる
さらに、リソースお片付け問題に関しても、defer を導入することで解決しています。
func openAndClose(filename string) { f, err := os.Open(filename) // snip defer f.Close() // openAndCloseを抜けるときに必ず呼ばれる // snip }
現代的な言語なのにいわゆる例外がないの!!!!って最初はびっくりしますが、goroutineとの絡みを考えると非常にバランスのとれた設計だと言えそうな気がしますし、ある意味「あんまり堅苦しくないしモナドじゃないEither」みたいな立ち位置で、かなり面白いですね!
エラーハンドリングのパラダイムをいろいろ見てみました。「どれが最高の正解」ってことはないけれど、それぞれのプラットフォームやパラダイムがどう問題を解決しようとしているのかを知ることで、より安全なアプリケーションを書く助けにはなるのではないでしょうか。
php で zip したい場合は、array_map を使う(!!)
<?php $arr1 = ["a", "b", "c"]; $arr2 = ["A", "B", "C"]; $arr3 = ["エー", "ビー", "シー"]; $zipped = array_map( function($a, $b, $c){ return [$a, $b, $c]; }, $arr1, $arr2, $arr3 ); $zipped; // [["a", "A", "エー"],["b", "B", "ビー"], ["c", "C", "シー"]]
PHPのarray_mapは配列を複数放りこむことができ、配列を複数放り込んだ場合はmapのわたす関数の引数にそれぞれの配列の要素が順に入ってくる。ことを利用するとzip的なことができるっぽい。
さらに面白いことに、関数の代わりにnullをわたすと上記のzip操作と同じ動きをする。なんでや!って感じするけど。
ちなみに、長さの違う配列を複数渡した場合、足りない分はNULLが渡ってくる。
<?php $arr1 = ["a", "b", "c"]; $arr2 = ["A", "B"]; $arr3 = ["エー", "ビー"]; $zipped = array_map( null, $arr1, $arr2, $arr3 ); $zipped; // [["a", "A", "エー"],["b", "B", "ビー"], ["c", null, null]]
このarray_map、他の言語ではあまり見かけない挙動のように思うし、これってarray_mapっていう名前でいいのかなって感じもするけど、zipとmapを一緒にやりたいときとかはこれはこれで便利っぽい気もする。というのも、PHPには遅延リストがない(よね?)ので、zipしてmapすると2回配列をトラバースすることになるけど、これなら一回のトラバースで良いので効率的ではある(zipとmapを一緒にやりたいときにのみ効いてくる効率化がどれほど重要かという話はあると思うけどまあそれはそれとしておいておきましょうよ)。PHPらしく、長さの違う配列渡したときにどっちに合わせるかを決めるオプションがありそう!!!とか思って探したけど、なかったっぽい。
see also: PHP: array_map - Manual
別に知見は書いてないですが、なるほどなーと思ったという感想を書いたエントリです。
ScalazとHaskellのFunctorの提供するmap(fmap)は、引数の順番が異なります。
def map[A, B](r: F[A])(f: A => B): F[B]
fmap :: f => (a -> b) -> f a -> f b
つまり第一引数と第二引数が逆。
ふつうに考えると Scalaz のやつが直感的に思えます。C言語とかでも、ある構造体を操作するための関数って大体第一引数にその構造体を渡して、他のパラメータをその後に渡すし、Perlとかだって $nyan->do_something したら $nyan が第一引数に渡ってくるし。
なんでなんでそうなっているのか調べたわけではないんだけど、関数のリフティングするときにはHaskellみたいになってたほうが便利だよなーと思い当たって「ふーむなるほど」となりました。
要するに、Haskell スタイルだと f :: (a -> b) な関数を g :: (f a -> f b) に変換したいというときに、なにも特別なことをしなくても fmap f
とすれば部分適応されるので自動的にそっから g :: (f a -> f b)を得ることができてうれしい。
一方 Scalaz はデータに注目した場合は直感的ではあるのだけれど、Liftingのためのメソッド lift の実装は def lift[A, B](f: A => B): F[A] => F[B] = map(_)(f)
となっていて、引数の順番が違うせいで一枚噛ませる必要がある。
これ、言語の特徴を捉えていて面白いな、と思いました。つまり、Scalaはオブジェクト指向的な考えで、第一の関心が「オブジェクト」の側にある。だから「最初にFunctor値を受け取って、それに対して関数を適用しますよ」という引数の順序のほうが自然なんだけど、liftみたいな"関数に主眼が置かれた操作"をやるときにScalazスタイルだと「引数の順番が逆だったらな〜」ってなる。
逆に、Haskellスタイルは第一の関心が「関数の側」にある。だから「関数をリフティングしたい」みたいなときは自然にかけるんだけど、データ(この例で言えば Functor値)のほうに注目していると、この引数の順序ってなんとなく不自然に思える。
で、思ったんですけど、これ、fmapに限らず、filterとかでもそうですね。Haskellはだいたい関数を第一引数に取るようになっていて、部分適応してやることによって新しい関数を作り出しやすいようになっている。
なるほどな〜〜〜という感じでした。
(さらに気づいたことを追記)
これ、型推論上の都合もありそう。型推論が左から右に流れるから、先にF[A]が来てないと、実際のFunctor値の型から A => B の A を推論してくれないという都合があるかもしれない。
個人の日記です。
id:moznion がはるばる新潟まできて肉をやってくれるというので好意に甘えて肉をやりました。
肉をはじめたところです。すでに仕込み済の肉をmoznionが持ってきてくれたので、焼くフェーズから開始している様子です。
焼いてくれています
このような見た目で最高な感じでした
うまい(確信)
こちらはアンティクーチョというペルー料理だそうです。ハツをスパイスとかビネガーとかに漬け込んで焼いたものだそうです。これ本当においしかったのでうちでもやろうと思った。
牡蠣も食べたかったので生牡蠣をレモンと塩でいただきました。
今回の功労者が青色のなにかを口元に押し当てている様子です。
肉はとにかくうまかったのでとてもよかったです。
昔から家にひとを呼ぶのは好きだったんですけど、息子が生まれてからはなかなかこういう機会を作ることができませんでした。でもやっぱり家にひとを呼びたくて、妻に相談したら快くOKしてくれて肉会開催の運びとなりました。今回は来てくれたひとたち(@moznion @hayajo @NPoi @sambaiz)がみんな息子の相手をしてくれて、息子もとても楽しそうにしていたので本当にみなさんありがとうございますという感じでした。
あとこういう遊びを理解してくれて協力までしてくれるししかもかわいいわたしの妻はまじで最高という感じなのでみなさんもわたしの妻と結婚するといいですよ。ぜったい渡さないけど。
ところで、ここからは自分語りです。
わたしが子どもの頃、実家では母が音楽教室をやっていたのですが、母がイベント好きなのもあって、我が家は様々なひとが訪れる場となっていました。いい年してけっこう人間としてダメな感じのギタリストのおじさんとか、民族楽器を作ったり叩いたり弾いたり作り方を教えるワークショップを全国旅しながらやってるドレッドのおじさんとか、そういうちょっと「変」なひとたちがよく家に訪れてました。
わたしはちょっと性格に難がある(ちょっとだと信じたい)ので、どうしても子供社会でうまくやっていくことができなくて、実際に小学校や中学校はかなり苦痛だったのですが、そういう様々なひとがわたしを可愛がってくれるというのが、とても救いでした。楽しそうに生きてる大人が「学校以外にも世界はあるんだぜ」ってその態度で教えてくれる子供時代で、それは本当に恵まれていたんだなぁ、と今では思っています。
だから、というわけではないですし、どっちかというと単純にわたしが遊びたいからなんですけど、今後も家に大人をガンガン呼んでいきたいなぁ。そして結果として息子が「楽しそうに遊んでる大人たち」の姿からなにかを感じ取ってくれたらそれは多分いいことなんじゃないかな、なんて思っています(まあまだ息子はわけわかってないと思うけどな!)。というわけで、今後も家にひとを呼びまくるんで友人各位は覚悟しておいてくださると幸いです!
著者近影です pic.twitter.com/TmbLpBAA3C
— 虚弱体質 (@moznion) 2015, 3月 8
社内でレビューおじさん業してて書いた内容ですけど守秘する必要ない情報なんでちょっと内容書き換えてオープンアンドシェアーします。
見た目とかUIというのはソフトウェアの中でめちゃめちゃ柔らかい部品(些細な変更されることが多い部品)なので、「同じような部品だから共通化しちゃおう」ってやると失敗することが多いです。
特に気をつけるべきなのは、たとえばコンテンツをランキング形式でテーブルで表示する画面と、新着から順にテーブルで表示する画面があって、このふたつのテーブル部分は一緒だからパーシャルにしちゃおう、みたいなやつです。
見た目とかUIというのはソフトウェアの中でめちゃめちゃ柔らかい部品、というのがここで効いてきて、「新着とランキングは基本的に同じ表示なんですけど、ランキングのほうではランクがアップしたかダウンしたかのアイコンを表示してほしいんですよね〜」とか言われたり、「今見てるページが新着かランキングかわかるように、こことこことこことここの色をページによって変えたいんだよね」とか言われたりすることは想像できますね。でもここでテンプレートがパーシャルになっちゃってたりすると、共通化したテンプレートにif type == :ranking みたいな分岐持たせることになったりしちゃいます。そうすると、それってもう共通化の意味ないよね〜状態になることが結構あります。
そんなわけで、テンプレートの共通化はかなり慎重にやったほうがいいと思います。「たまたま同じような表示であるのか」それとも「将来にわたって同じ表示なのか」っていうのはなかなかわかんないものですから。
じゃあどういうときにパーシャル使うべきかっていうと、複数ページで共有されるフッターとか、ウィジェット的なやつとか、グローバルヘッダーとか、そういう部分はどんどんパーシャルにしちゃえばいいと思います。こういうのは全ページで同じであることが(基本的には)保証されてたほうがいいんで。
共通にしといて、if文とかで分岐させないとだめになったらそのタイミングでバラすというのも手だとは思います。ただ、複数人で開発とかしてると設計の変更って分岐追加するよりMP消費するから、どうしても最初の設計にひっぱられがちになるというのは感じてて、今回の例のように関心がそもそも違うみたいな場合は別にしちゃうほうが筋が良さそうだなーと思います。