プログラマとコミュニティ あるいは SD8月号に記事を書かせてもらいました

表題にあるとおり、Software Design8月号の特集記事を書かせていただきました。GitHub入門的な記事です。

今書店で売ってる号は7月号ですので、今売ってるやつには載ってません(でももちろん買ってくださってもいいんですよ?)。発売日は7/18ですので、その後書店で購入したり、Amazonなどでお求めください。

www.amazon.co.jp

今年に入ってから、web+DB Pressの91,92号、SD8月号と、技術評論社さんの雑誌に記事を書かせていただく機会を立て続けにいただけて、ほんとうにありがたい限りです。

ところで、web+DB vol. 91の記事はYAPC::Asiaでの発表がきっかけでチャンスをいただいたのでした。92はPerlコミュニティつながりで頂いたお話でした。今回の記事に関しては、「Gitをはじめからていねいに」というドキュメントをGitHub上で公開しているのを目にとめていただいてお声がけいただくという経緯でした。

立て続けにこういう機会をいただき、拙いながらもアウトプットを続けていると、目にとめてくださるひとたちはいるのだなぁ、続けるというのは大事だなあというのを実感しています。ありがたいことです。

さて、このあとは壮大な蛇足です。

アウトプットのモチベーションの話

ところで、話が急に変わるようですが、わたしは文学部出身プログラマです。学生時代にきちんとコンピュータサイエンスを学んだことはなく、プログラミングは独学でやってきている人間です(そのため、きちんとしたコンピュータサイエンスのバックグラウンドに支えられた「基礎体力」のある方々に対するコンプレックスが結構強くあるのですが、それはまた別の話なのでおいておきます)。

「独学で」と言いましたが、それは決して「独力で」学ぶことはできないものです。わたしが主戦場としているいわゆるweb系は特にそうだと思うのですが、インターネットには、プログラマコミュニティが書き残してくれた膨大な数の知見が積み重なっています。もちろん、書籍から学ぶことも多かったのですが、それと同じくらい、インターネット上にアーカイブされた素晴らしい情報たちに、わたしはプログラマとして育てられてきましたし、現在も育てられています。

中でも、勉強会のスライドや発表には質の高いものが多いと感じます。これは、プログラマコミュニティがオープンにしてきてくれた、プログラマコミュニティの財産だと感じています。この財産を分けてもらうことで、独力ではできない独学が可能になっているとわたしは強く感じています。

さらに、インターネット上の情報だけではなく、なまのコミュニケーションも、わたしをとても育ててくれています。特に、Hachioji.pmで出会った方々、Niigata.pmで出会った方々、NDSで出会った方々、@9mさんを通じて知り合った方々とは、心理的に近い位置で技術的な相談をしたりされたりする中で、互いに切磋琢磨することができていて、この関係もまたわたしをプログラマとして現在進行形で育て続けてくれています。(この前のヤパチーでもshiba_yuさんやninjinkunさんとコミュニケーションできたことは大きな喜びでした!)

実は、わたしが技術的なアウトプットするモチベーションは、わたしを育ててくれているそんなコミュニティに対する恩返しがしたい、という気持ちが支えています。まあ、とはいえぶっちゃけ、そんなきれいごとだけじゃなくて、承認欲求にドライブされてる部分もめちゃめちゃ多くあるんですけど。でも、それだけではやっぱりやっていけないんですよね。クソリプ的な反応に心折れるし。わたしは、「ほんとうに多くのものをコミュニティや友人たちから受け取っているわたしが、自分の力でできることってなんだろう」という気持ちがなければ継続したアウトプットはできません。

アウトプットをしてたらいいことがあった

で、そうやってアウトプットを続けていたら、いいことがたくさんわたしに降りかかりました。

会社を超えて、プログラマ仲間がたくさんできました。その仲間たちは力強くて、わたしの知らないことをたくさん知っていて、しかもわたしが相談すると惜しみなくその知恵を貸してくれます。

転職や就職ができました。「コネ」ってわけではないけれど、アウトプットをたくさんした結果、「このひとはこれこれこういうことが得意でこういうことはあんまり得意ではないんだな」というのを分かった上で声をかけてくれるので、お互いにミスマッチをあまり心配せずに転職活動や就職活動を行うことができ、これはかなりありがたいことです。

そして、最初の話にようやくつながるのですが、雑誌の記事を書かないか、とお声がけいただくことができました。以前このブログにも書いたことがありますが、昔の夢が思わぬところでかなったりもしました。ありがたい話です。

でも、こんなの全部副次的なものです(とはいえ実際、かなり大きなメリットも享受しているのは無視できない事実だけれど、まあ、気持ちの問題として)。わたしにとっていちばん大きな喜びは、コミュニティから得た財産を、新しいだれかに渡すチャンスをたくさん得られるようになったことです。わたしの大好きな小沢健二の楽曲のワンフレーズに「愛すべき生まれて育ってくサークル 君や僕を繋いでる緩やかな止まらない法則」というのがありますが、まさにその法則のほんの一部にでも自分がなれているという実感こそが、わたしがアウトプットを経て得られているいちばんの果実です。

もし燻ってるひとがいたら、ぜひ一歩を踏み出してみたらいいんじゃないかという話

ようやくこの文章の結論にたどり着きました。だから、もし今この文章を読んでくれてるあなたが、プログラミングが好きで、なおかつアウトプットすることに興味があったりコミュニティに興味があるんだけど、それでもなんか一歩が踏み出せない、なんて状態で悶々としてるなら(そうじゃないならごめんなさい)、心配せずにその一歩を踏み出してみたらいいと思います。

もちろん、わたしはかなり運と縁に恵まれた例であるということは否定できません。なので、生存バイアス的なあれがそれしてる部分もあると思います。でも、プログラミングしてたら、みんな多かれ少なかれコミュニティから何かをもらってるんじゃないでしょうか。だったら、コミュニティからなにかをもらい、自分も誰かになにかを与えるというサークルの中に、一歩踏み込んでみるのは、単なるメリット云々を超えた意味があるとわたしは思います。

ブログを書く、というのもひとつの方法ですが、勉強会に登壇して仲間を見つけるという、ちょっと勇気が必要だけど大きな一歩を、この文章を読んだだれかが踏み出してくれて、上述のようなサークルがまたひとつどこかで産まれて育っていくようなことがあったら、わたしはとても嬉しく思います。

なんか大した実績があるわけでもなければロックスターでもないのに偉そうな言い方になってしまったけれど、「そういうの興味ねーよ」とか「お前に言われるようなことじゃねーよ」ってなってたらごめんなさい。エモいおっさんの戯言だと思って聞きながしてブコメあたりにでも「黙ってコードを書けよハゲ」「で、誰?」とでも書き残して叱責しておくれ。

Perlの黒魔術を解説するよ〜〜〜〜

まずはこちらをごらんください。

shinh.hatenablog.com

すごすぎる……。恐ろしいですね。

なぜこんなことになるのか、解説していきましょう。まずはPerlの気持ちになりましょう。

Perlの気持ち編

ポイントその1 barewordを数値コンテキストで評価するとどうなるのかということ

件のプログラムは、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が続いていますね。ではそれぞれの項がどう評価されるか見てみましょう。

ポイントその2 s//vなんか/e という項

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の評価結果を文字列コンテキストで挿入してるわけですね。そして、これを繰り返すことで、$_に文字列を貯めていきます。

ポイントその3 s//なんか/0からn文字のs という項について

これは「0からn文字のs」が置換正規表現のオプションとして扱われますね。ではsというオプションは何を意味するでしょうか。これは「ワイルドカードのドット( . )が改行にもマッチするようにする」というオプションです。今回は改行使ってないので、この「0からn文字のs」については無視して良いことになりますね。

さて、そうなると、s//なんか/0からn文字のs の評価結果は、$_ の先頭に「なんか」の部分の文字列を貯めていくことになるわけです。

ポイントその4 最後の項

さて、最後はs//use/s/evalという項です。

これはちょっとトリッキーですが、Perlの気持ちになって読むと、(今まで解釈してきた部分 + s//use/s) / (eval) という割り算として解釈できます。 / の優先順位は + より高いですからね。 つまり、今までは単純に前から読んできたけど、評価順としては、(今まで読んできた部分 + s//use/s) を評価して、そのあと(eval) を評価して、最後に / で割り算する、という形ですね。でも割り算の結果は捨てられてるので、結果的には前から順に評価されてるのと同じことです。

では最後の部分をみてみましょう。s//use/s についてはさきほど見たとおりですね。 $_ の先頭に "use" をappendするように解釈されます。そして、そのあと(eval) が評価されるわけですが、evalは引数が省略された場合、$_を暗黙の引数として取ります。これで、「最後の項を除く部分を評価した結果得られた $_ をevalで評価する」というプログラムの完成です。

では、今まで見てきたものをまとめましょう。

Perlの気持ち解決編

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デコーダだ!!!

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の'$_'と、ヴァージョン文字列とかいう変態仕様を最大限悪用してるんですよ。すごすぎる!!!という思いを得ることになりましたね!!!!世の中どんだけ天才がいるんだよ……。

エラーハンドリング・クロニクル #nds41

はじめに

プログラミング技術の歴史は、ありとあらゆる歴史がそうであるように、いろんな「史観」で眺めることができます。ならば、プログラミング技術の歴史を、「エラーハンドリングとの戦い」という視点から見ることもできるのではないでしょうか。本日は、エラーハンドリングとの戦いの歴史を俯瞰することで、エラーハンドリングの勘所について考えていこうと思います。

なお、このエントリはNDSという勉強会の第41回で発表した内容と同一です。

Cの時代

Cの時代のエラーハンドリングでは、関数の返り値と、グローバル変数errnoを見ることで処理が成功したか失敗したかを見るのが一般的でした。

例として、文字列をlongに変換するstrtol関数をmanで引いてみましょう。すると、だいたい以下のようなことが書かれています。

  • 変換に失敗すると、0を返す
  • 変換に失敗した場合、グローバルな変数であるerrnoに以下の定数を格納する
    • invalidな文字列を渡された:EINVAL
    • longをoverflowしたりunderflowする数を渡された:ERANGE

さて、一見してとてもシンプルな仕様ですが、現代的な視点からみてみると、いくつか気になることがありそうです。ざっと挙げてみると……

  • 関数を呼び出したあと、エラーであるかどうかを手動でチェックする必要があるし、仮にチェックしなかったとしてもコンパイラが怒ってくれるわけではない
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 では依然として別スレッドの例外を補足する方法はなく、非同期処理と例外機構というのは、やはり結構相性が悪いもののようです。

Eitherの時代

さて、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について

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」みたいな立ち位置で、かなり面白いですね!

まとめ

エラーハンドリングのパラダイムをいろいろ見てみました。「どれが最高の正解」ってことはないけれど、それぞれのプラットフォームやパラダイムがどう問題を解決しようとしているのかを知ることで、より安全なアプリケーションを書く助けにはなるのではないでしょうか。

Functor における map の引数の順序を考えてたらいっこストンと腑に落ちた話

別に知見は書いてないですが、なるほどなーと思ったという感想を書いたエントリです。

ScalazとHaskellのFunctorの提供するmap(fmap)は、引数の順番が異なります。

  • Scalaz の Functor

    • def map[A, B](r: F[A])(f: A => B): F[B]
    • F[A] なFunctor値が最初の引数で、A => B な関数が次の引数で、F[B]なファンクター値が返り値
  • Haskell の Functor

    • fmap :: f => (a -> b) -> f a -> f b
    • (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 を推論してくれないという都合があるかもしれない。

テンプレートをDRYにするのは慎重にやったほうがいいですよねというお話

社内でレビューおじさん業してて書いた内容ですけど守秘する必要ない情報なんでちょっと内容書き換えてオープンアンドシェアーします。

本文

見た目とかUIというのはソフトウェアの中でめちゃめちゃ柔らかい部品(些細な変更されることが多い部品)なので、「同じような部品だから共通化しちゃおう」ってやると失敗することが多いです。

特に気をつけるべきなのは、たとえばコンテンツをランキング形式でテーブルで表示する画面と、新着から順にテーブルで表示する画面があって、このふたつのテーブル部分は一緒だからパーシャルにしちゃおう、みたいなやつです。

見た目とかUIというのはソフトウェアの中でめちゃめちゃ柔らかい部品、というのがここで効いてきて、「新着とランキングは基本的に同じ表示なんですけど、ランキングのほうではランクがアップしたかダウンしたかのアイコンを表示してほしいんですよね〜」とか言われたり、「今見てるページが新着かランキングかわかるように、こことこことこことここの色をページによって変えたいんだよね」とか言われたりすることは想像できますね。でもここでテンプレートがパーシャルになっちゃってたりすると、共通化したテンプレートにif type == :ranking みたいな分岐持たせることになったりしちゃいます。そうすると、それってもう共通化の意味ないよね〜状態になることが結構あります。

そんなわけで、テンプレートの共通化はかなり慎重にやったほうがいいと思います。「たまたま同じような表示であるのか」それとも「将来にわたって同じ表示なのか」っていうのはなかなかわかんないものですから。

じゃあどういうときにパーシャル使うべきかっていうと、複数ページで共有されるフッターとか、ウィジェット的なやつとか、グローバルヘッダーとか、そういう部分はどんどんパーシャルにしちゃえばいいと思います。こういうのは全ページで同じであることが(基本的には)保証されてたほうがいいんで。

追記

共通にしといて、if文とかで分岐させないとだめになったらそのタイミングでバラすというのも手だとは思います。ただ、複数人で開発とかしてると設計の変更って分岐追加するよりMP消費するから、どうしても最初の設計にひっぱられがちになるというのは感じてて、今回の例のように関心がそもそも違うみたいな場合は別にしちゃうほうが筋が良さそうだなーと思います。

値の一意性を保証したいときに気をつけるべきこと

たまに以下のようなロジックで値の一意性を保証しようとしているコードを見かけます。

if ( 既に値が存在するか ) { ...(1)
    print "別の値にしてください"
} else {
    値をどっかに保存する処理 ...(2)
    print "保存しました"
}

一見うまく動きそうなんですけど、これは複数プロセスや複数スレッドが同時に実行されるような環境ではうまく動きません。

例えばwebアプリでDBに値を保存する場合などは、こういうことが考えられます。

  1. ユーザーAが nyan という値を送ってきます
  2. まだ DB には nyanという値が存在しないので、 (1) で else 節に入ります
  3. ここでユーザーBも偶然 nyan という値を送ってきました
  4. まだ DB には nyan という値が存在しないので、 (1) で else 節に入ります
  5. else 節に入ったユーザーAのリクエストは(2)の行を処理し、正常に終了します。
  6. else 節に入ったユーザーBのリクエストは(2)の行を処理しようとします。

このとき、DBにuniq制約が貼ってあればユーザーBには想定していないエラーを返すことになりますし、uniq制約が貼っていない場合、重複してはいけないはずのデータなのに重複を許してしまうという最悪の事態になります。

こうならないように、値の一意性を確保したい場合は、より低く確実な層でチェックするべきでしょう。今回の場合なら、DBにuniq制約を貼った上で以下のようなロジックにするべきです。

try {
    値をどっかに保存する処理
    print "保存しました" 
} catch (ユニーク制約に引っかかったよ例外) {
    print "別の値にしてください"
}

// あるいは

error = 値をどっかに保存する処理
if (error_code == NULL) {
    print "保存しました" 
} elseif (error_code == ユニーク制約に引っかかったよエラー) {
    print "別の値にしてください"
} else {
    print "予期しないエラー"
}

DBへの保存の他にも、例えば「もし存在してないなら、新しくディレクトリを作る」みたいなやつに関しても気をつけなければいけません。これも、「先にチェックして、無かったら作る」みたいにしちゃうと同じようなパターンで予期しないエラーを引き起こす可能性があります。それを防ぐためには、

  1. まずmkdir してしまって
  2. エラーが出なければ新規作成されたので正常系
  3. EEXIST 以外のエラーならば予期していないエラー
  4. EEXIST なエラーである場合
    1. path がディレクトリであれば作成済みであったので正常系
    2. path がディレクトリでなければ、「同名のファイルがあるよ」というエラー

といった形にしなければなりません。

要するに、重複が許されないようなものに関しては、自分でアプリケーションのレイヤーでそのチェックを組むと並行性に問題が出てくるので、アプリケーションのレイヤーより下に任せた上で、その結果をアプリケーション側でハンドルしてあげるようにしましょうね、という話でした。

もちろんどこまで真面目にやるべきかは案件によるとしか言えないし、多くの場合、データの整合性が守られてるならそれで問題なしとしていいことのほうが多いとは思う。でもこういうの知っておかないと困ることもあるので一応知っておいたうえで「そこまで真面目にやる必要なし」という判断ができるようになっていたほうがよいだろうという話。

以下余談。

RailsActiveRecord の uniqueness validation が、まさに上のような問題を持っている。これはまあいろんなDBに対応しないといけない以上しょうがない部分があるし、どちらにせよDBにuniq制約貼っておけばデータの整合性が破壊されることはないので、そこまで大きな問題ではないんだけど、上記のようなパターンで ActiveRecord::RecordNotUnique 吐かれた場合に真面目にユーザーに「別の値使ってください」って通知しようとするときにはどうするべきなんだろうなぁというのを迷っている。ActiveRecord::RecordNotUnique 例外を catch して record.errors に add するようなやつを ActiveRecord::Validations::UniquenessValidator に prepend しちゃう?それはちょっと暴力的すぎるなぁ、などなど悩みは尽きない。だいたい毎回「レアケースだし、データが守られてればそこまで真面目にやらんでええやろ」に落ち着く気がする。

LLユーザこそ「ふつうのLinuxプログラミング」を読んだらいいという話

4年くらいまえに一度通読して「ふーん」で終わってたんだけど、最近読み返したら「これはめっちゃ良い本では?」となったので紹介します。

特に第二部、第三部がめっちゃよくて、Linuxの重要な概念を、押さえるべきところを押さえて説明してあって最高な感じでした。

サブタイトルにこそ「Linux の仕組みから学べる gcc プログラミングの王道」とありますが、むしろ普段から C 言語でシステムコールに近い部分を普通に書いているプログラマよりも、普段は PerlRuby などのスクリプト言語を使っているが、その言語で書かれたコードがプロセスの内外部でOSとどのようなやり取りをしているのかを知りたい、といったプログラマにこそ読んでほしい本です。

というのも、本書のサンプルコードはその性質上 C 言語で書かれていますが、その内容はかなり平易なものです。「C言語バリバリ読み書きしてるぞ!!!!」みたいなひとにはむしろ物足りないかと思われる一方で、普段 LL 書いてるプログラマにとっても十分に読みやすいと思います。この本の一番の魅力(だと私が感じたの)はここですね。要するに「平易な C 言語のコードでもって Linux の仕組みが概観できる本」なんです。

というわけで、サーバーサイドのプログラミングを LL でやってるんだけど、LL よりも少し低レイヤーになると理解が怪しくて、トラブルシューティングに時間がかかったり自信が持てなかったりする、みたいなひとにこそおすすめできる感じの良書でした。

ちなみに、ここで言う「平易なC言語」というのは、さすがに「ポインタとか構造体とかがわからん」とかだと読めないと思うけど、C言語の入門書が一通り読めて一通り理解できてたらひっかかるところはないであろう、くらいの感じです。