…という事実だけを記しておく。
なんとか2月にズレこむのだけは避けられた。
[amazonjs asin=”4274219151″ locale=”JP” title=”プログラミングElixir”]
takatoh's blog – Learning programming languages.
…という事実だけを記しておく。
なんとか2月にズレこむのだけは避けられた。
[amazonjs asin=”4274219151″ locale=”JP” title=”プログラミングElixir”]
去年の暮れに、今年は Elixir をやる!と言っておきながらいまだにやってない。それどころか、C で MD5 ハッシュ値を計算するプログラムを作っていて行き詰ってしまった。
きっかけは、MD5 の計算アルゴリズムが意外にシンプルだとを知ったこと。具体的には次のページを見たことだ。
cf. MD5 メッセージダイジェストアルゴリズム
cf. MD5のアルゴリズム – 数学自由研究
確かに、考えていたよりは単純な計算で、それほど引っかかるようなところもなく MD5 ハッシュ値らしい文字列を出力するところまでは実装できた(もちろん、久しぶりの C なので些細な文法ミスで引っかかるところはあったけれど)。
が、結果が合わない。試しに sample.zip ファイルのハッシュ値を、いつも使っている md5make というソフトで求めてみる(GUI のソフトだけど、<ファイル名>.md5 というファイルにハッシュ値を書き出してくれる)と:
^o^ > cat sample.zip.md5 d7db18d8239cca197c66175a8cfb0e85
これに対して、オレの書いた md5hash プログラムの出力は:
^o^ > md5hash sample.zip ece936c4aefc960c882f49b2015aac27
となる。
コードはこのエントリに最後に載せるけど、md5hash にはデバッグのためのオプション -d
と -t
をつけてみた。-d
オプションは、対象のファイルから読み込んだブロック(MD5 アルゴリズムは 32 ビットを 1 ワードとして、16 ワードずつ処理するのでこれをブロックと呼んでいる)を表示するオプション。データ長を 16 ワードに合わせるためのパディングなども含めて出力する。-t
オプションは、計算に使う定数テーブルを出力するオプション。
どちらのオプションの出力も、見た限りでは間違っていないように見える。
となると、あとは実際の計算をしている部分に間違いがありそうだということになるんだけど、個々の関数やその呼び出し引数をチェックしてみても、間違ったところは見当たらない。
というわけで、行き詰っている。どうしよう。
とにかく、書いたコードを載せておく。しばらくして何か思いつくかもしれない。
#include #include #include #include "md5.h" void Usage(void); int main(int argc, char *argv[]) { FILE *fp; WORD block[16]; int p; int count = 0; int i; char hex[33]; CONTEXT *context = NULL; unsigned long t[64]; char md5hash[33]; int opt, opt_dump = 0, opt_table = 0, option_index; struct option long_options[] = { {"dump", no_argument, NULL, 'd'}, {"table", no_argument, NULL, 't'}, {"help", no_argument, NULL, 'h'}, {0, 0, 0, 0} }; while ((opt = getopt_long(argc, argv, "dth", long_options, &option_index)) != -1) { switch(opt) { case 'd': opt_dump = 1; break; case 't': opt_table = 1; break; case 'h': Usage(); exit(0); case '?': printf("Unkown option -%c\n", optopt); Usage(); exit(1); } } if (opt_dump) { fp = fopen(argv[optind], "rb"); while (1) { p = ReadBlock(fp, block); if (p == 64) { printf("block readed: %d\n", ++count); for (i = 0; i < 16; i++) { HexEncode(block[i], hex); printf(" %s\n", hex); } } else { printf("rest %d bytes\n", p); for (i = 0; i < 16; i++) { HexEncode(block[i], hex); printf(" %s\n", hex); } break; } } fclose(fp); } else if (opt_table) { InitTable(t); for (i = 1; i<= 64; i++) { HexEncode(t[i - 1], hex); printf("%2d: 0x%s\n", i, hex); } } else { context = (CONTEXT *) malloc(sizeof(CONTEXT)); InitContext(context); InitTable(t); fp = fopen(argv[optind], "rb"); MD5Calc(fp, context, t, md5hash); fclose(fp); printf("%s\n", md5hash); free(context); } return 0; } void Usage(void) { printf("Usage: md5hash [-d | -t | -h] \n"); return; }
typedef unsigned long WORD; // 32 bits = 4 bytes typedef struct tagContext { WORD A; WORD B; WORD C; WORD D; } CONTEXT; void InitContext(CONTEXT *context); void InitTable(unsigned long *t); void HexMD5(CONTEXT *context, char *hex); int ReadBlock(FILE *fp, WORD *block); int CalcPaddingSize(int rest); void CharToBlock(unsigned char *b, WORD *block, int s, unsigned long long filesize); WORD ULongToWORD(unsigned long n); void HexEncode(WORD n, char *hex); void HexEncode2(WORD n, char *hex); WORD F(WORD x, WORD y, WORD z); WORD G(WORD x, WORD y, WORD z); WORD H(WORD x, WORD y, WORD z); WORD I(WORD x, WORD y, WORD z); WORD BitRotate(WORD x, int n); WORD FF(WORD a, WORD b, WORD c, WORD d, WORD x, int s, unsigned long t); WORD GG(WORD a, WORD b, WORD c, WORD d, WORD x, int s, unsigned long t); WORD HH(WORD a, WORD b, WORD c, WORD d, WORD x, int s, unsigned long t); WORD II(WORD a, WORD b, WORD c, WORD d, WORD x, int s, unsigned long t); void MD5Calc(FILE *fp, CONTEXT *context, unsigned long *t, char *hex); void RoundBlock(CONTEXT *context, WORD *block, unsigned long *t);
/* * MD5 hash calculation library * * cf. http://www.ipa.go.jp/security/rfc/RFC1321JA.html * cf. http://azisava.sakura.ne.jp/math/md5.html * */ #include #include #include #include #include "md5.h" void InitContext(CONTEXT *context) { context->A = 0x67452301; context->B = 0xEFCDAB89; context->C = 0x98BADCFE; context->D = 0x10325476; return; } void HexMD5(CONTEXT *context, char *hex) { char hex_a[9], hex_b[9], hex_c[9], hex_d[9]; int i; HexEncode2(context->A, hex_a); HexEncode2(context->B, hex_b); HexEncode2(context->C, hex_c); HexEncode2(context->D, hex_d); for (i = 0; i < 8; i++) { hex[i] = hex_a[i]; } for (i = 8; i < 16; i++) { hex[i] = hex_b[i - 8]; } for (i = 16; i < 24; i++) { hex[i] = hex_c[i - 16]; } for (i = 24; i < 32; i++) { hex[i] = hex_d[i - 24]; } hex[32] = '\0'; return; } int ReadBlock(FILE *fp, WORD *block) { int r; unsigned char b[64]; unsigned long long filesize; if ((r = fread(b, 1, 64, fp)) < 64) { filesize = ftell(fp); CharToBlock(b, block, r, filesize * 8); } else { CharToBlock(b, block, r, 0); } return r; } int CalcPaddingSize(int rest) { int p; if (rest < 56) { p = 56 - rest; } else if (rest == 56) { p = 64; } else { p = 64 - rest + 56; } return p; } void CharToBlock(unsigned char *b, WORD *block, int s, unsigned long long filesize) { int i, j, k = 0; int p; unsigned long bk[16]; unsigned char c[64]; unsigned long fs_u, fs_l; if (s == 64) { for (i = 0; i < 64; i += 4) { bk[k] = 0; for (j = 0; j < 4; j++) { bk[k] <<= 8; bk[k] = bk[k] + b[i + j]; } k++; } } else { for (i = 0; i < s; i++) { c[i] = b[i]; } p = CalcPaddingSize(s); c[i] = 0x80; i++; for (; i < s + p; i++) { c[i] = 0; } fs_l = (unsigned long) filesize; filesize >>= 32; fs_u = (unsigned long) filesize; for (i = 63; i >= 60; i--) { c[i] = fs_u; fs_u >>= 8; } for (i = 59; i >= 56; i--) { c[i] = fs_l; fs_l >>= 8; } for (i = 0; i < 64; i += 4) { bk[k] = 0; for (j = 0; j < 4; j++) { bk[k] <<= 8; bk[k] = bk[k] + c[i + j]; } k++; } } for (i = 0; i < 16; i++) { block[i] = ULongToWORD(bk[i]); } return; } WORD ULongToWORD(unsigned long n) { unsigned char b; WORD w = 0; int i; for (i = 0; i < 4; i++) { b = (unsigned char) n; w = (w << 8) + b; if (i < 3) { n = n >> 8; } } return w; } void HexEncode(WORD n, char *hex) { int r, i, j; char t[16] = {'0', '1', '2', '3', '4', '5', '6', '7','8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; i = 0; hex[0] = '\0'; while (1) { r = n % 16; for (j = i; j >= 0; j--) { hex[j + 1] = hex[j]; } hex[0] = t[r]; i++; n = n / 16; if (n == 0) { break; } } if (i < 8) { for (; i < 8; i++) { for (j = i; j >= 0; j--) { hex[j + 1] = hex[j]; } hex[0] = '0'; } } return; } void HexEncode2(WORD n, char *hex) { char h[9]; HexEncode(n, h); hex[0] = h[6]; hex[1] = h[7]; hex[2] = h[4]; hex[3] = h[5]; hex[4] = h[2]; hex[5] = h[3]; hex[6] = h[0]; hex[7] = h[1]; hex[8] = '\0'; return; } /* * Initialize constants table */ void InitTable(unsigned long *t) { double p; int n; p = pow(2.0, 32.0); for (n = 1; n <= 64; n++) { t[n - 1] = (unsigned long) (p * fabs(sin(n))); } return; } /* * MD5 calcrate function */ void MD5Calc(FILE *fp, CONTEXT *context, unsigned long *t, char *hex) { WORD block[16]; int p; while (1) { p = ReadBlock(fp, block); RoundBlock(context, block, t); if (p < 64) { break; } } HexMD5(context, hex); return; } void RoundBlock(CONTEXT *context, WORD *X, unsigned long *T) { WORD a = context->A; WORD b = context->B; WORD c = context->C; WORD d = context->D; // round 1 a = FF(a, b, c, d, X[0], 7, T[0]); d = FF(d, a, b, c, X[1], 12, T[1]); c = FF(c, d, a, b, X[2], 17, T[2]); b = FF(b, c, d, a, X[3], 22, T[3]); a = FF(a, b, c, d, X[4], 7, T[4]); d = FF(d, a, b, c, X[5], 12, T[5]); c = FF(c, d, a, b, X[6], 17, T[6]); b = FF(b, c, d, a, X[7], 22, T[7]); a = FF(a, b, c, d, X[8], 7, T[8]); d = FF(d, a, b, c, X[9], 12, T[9]); c = FF(c, d, a, b, X[10], 17, T[10]); b = FF(b, c, d, a, X[11], 22, T[11]); a = FF(a, b, c, d, X[12], 7, T[12]); d = FF(d, a, b, c, X[13], 12, T[13]); c = FF(c, d, a, b, X[14], 17, T[14]); b = FF(b, c, d, a, X[15], 22, T[15]); // round 2 a = GG(a, b, c, d, X[1], 5, T[16]); d = GG(d, a, b, c, X[6], 9, T[17]); c = GG(c, d, a, b, X[11], 14, T[18]); b = GG(b, c, d, a, X[0], 20, T[19]); a = GG(a, b, c, d, X[5], 5, T[20]); d = GG(d, a, b, c, X[10], 9, T[21]); c = GG(c, d, a, b, X[15], 14, T[22]); b = GG(b, c, d, a, X[4], 20, T[23]); a = GG(a, b, c, d, X[9], 5, T[24]); d = GG(d, a, b, c, X[14], 9, T[25]); c = GG(c, d, a, b, X[3], 14, T[26]); b = GG(b, c, d, a, X[8], 20, T[27]); a = GG(a, b, c, d, X[13], 5, T[28]); d = GG(d, a, b, c, X[2], 9, T[29]); c = GG(c, d, a, b, X[7], 14, T[30]); b = GG(b, c, d, a, X[12], 20, T[31]); // round 3 a = HH(a, b, c, d, X[5], 4, T[32]); d = HH(d, a, b, c, X[8], 11, T[33]); c = HH(c, d, a, b, X[11], 16, T[34]); b = HH(b, c, d, a, X[14], 23, T[35]); a = HH(a, b, c, d, X[1], 4, T[36]); d = HH(d, a, b, c, X[4], 11, T[37]); c = HH(c, d, a, b, X[7], 16, T[38]); b = HH(b, c, d, a, X[10], 23, T[39]); a = HH(a, b, c, d, X[13], 4, T[40]); d = HH(d, a, b, c, X[0], 11, T[41]); c = HH(c, d, a, b, X[3], 16, T[42]); b = HH(b, c, d, a, X[6], 23, T[43]); a = HH(a, b, c, d, X[9], 4, T[44]); d = HH(d, a, b, c, X[12], 11, T[45]); c = HH(c, d, a, b, X[15], 16, T[46]); b = HH(b, c, d, a, X[2], 23, T[47]); // round 4 a = II(a, b, c, d, X[0], 6, T[48]); d = II(d, a, b, c, X[7], 10, T[49]); c = II(c, d, a, b, X[14], 15, T[50]); b = II(b, c, d, a, X[5], 21, T[51]); a = II(a, b, c, d, X[12], 6, T[52]); d = II(d, a, b, c, X[3], 10, T[53]); c = II(c, d, a, b, X[10], 15, T[54]); b = II(b, c, d, a, X[1], 21, T[55]); a = II(a, b, c, d, X[8], 6, T[56]); d = II(d, a, b, c, X[15], 10, T[57]); c = II(c, d, a, b, X[6], 15, T[58]); b = II(b, c, d, a, X[13], 21, T[59]); a = II(a, b, c, d, X[4], 6, T[60]); d = II(d, a, b, c, X[11], 10, T[61]); c = II(c, d, a, b, X[2], 15, T[62]); b = II(b, c, d, a, X[9], 21, T[63]); context->A += a; context->B += b; context->C += c; context->D += d; return; } /* * MD5 basic functions */ WORD F(WORD x, WORD y, WORD z) { return (x & y) | (~x & z); } WORD G(WORD x, WORD y, WORD z) { return (x & z) | (y & ~z); } WORD H(WORD x, WORD y, WORD z) { return x ^ y ^ z; } WORD I(WORD x, WORD y, WORD z) { return y ^ (x | ~z); } /* * Bit rotation */ WORD BitRotate(WORD x, int n) { return x << n | x >> (32 - n); } /* * MD5 round functions */ WORD FF(WORD a, WORD b, WORD c, WORD d, WORD x, int s, unsigned long t) { WORD aa; aa = BitRotate(a + F(b, c, d) + x + t, s); return b + aa; } WORD GG(WORD a, WORD b, WORD c, WORD d, WORD x, int s, unsigned long t) { WORD aa; aa = BitRotate(a + G(b, c, d) + x + t, s); return b + aa; } WORD HH(WORD a, WORD b, WORD c, WORD d, WORD x, int s, unsigned long t) { WORD aa; aa = BitRotate(a + H(b, c, d) + x + t, s); return b + aa; } WORD II(WORD a, WORD b, WORD c, WORD d, WORD x, int s, unsigned long t) { WORD aa; aa = BitRotate(a + I(b, c, d) + x + t, s); return b + aa; }
PNG は透過色をサポートしている。で、タイトルに書いたように背景が透過色(というか透明)な画像から ImageMagic の convert
コマンドで JPEG のサムネイルを作ると、もともと透明だった背景が黒くなってしまう。
これまでサムネイルだからいいか、と放置してたんだけど、↓このページで解決法を見つけた。
cf. 透明度を含む画像を JPEG に変換する時の背景色 – [awm-Tech]
これまではこうやってサムネイルを作ってた。
takatoh@apostrophe $ convert -thumbnail 200x200 src.png thumbnail.jpg
だけどこれだと背景が黒くなってしまう。
そこでこうする。
takatoh@apostrophe $ convert -thumbnail 200x200 -flatten src.png thumbnail2.jpg
これだと背景が白くなる。-flatten
オプションの意味が、調べてもいまいちよくわからなかったんだけど、元画像の後ろに白いレイヤーを重ねているみたいだ。-background
オプションも使えて、例えば次のようにすると背景が赤くなる。
takatoh@apostrophe $ convert -thumbnail 200x200 -flatten -background red src.png thumbnail3.jpg
-background
のデフォルトが白みたい。
ともかく、これからは -flatten
をつけるようにしよう。
cf. 20 次にどこへ行こう – Where to go next – Elixir
さて、今年ももうすぐ終わりだ。
12月の初めから1月弱をかけて Elixir のチュートリアル(GETTING STARTED)をやってきたわけだけども、Ruby 風の文法の中に Haskell のようなパターンマッチングがあったりと結構面白い言語だと思った。特にパイプ演算子(|>
)で関数を数珠つなぎにしてデータを流していくところなんか、Haskell にもなく、新鮮に感じた。
というわけで、本を買った。来年はこれをやるぞ!
[amazonjs asin=”4274219151″ locale=”JP” title=”プログラミングElixir”]
cf. 19 シギル(印) – Sigils – Elixir
シギルとは ~
+一文字から始まる文字表現によってデータを記述するものだ。Ruby の % 記法みたいなもの。
最もよく使うのは正規表現だ。正規表現のシギルは ~r
で始まる。
iex(1)> re = ~r/foo|bar/ ~r/foo|bar/ iex(2)> "foo" =~ re true iex(3)> "baz" =~ re false
~r
の後にセパレータ(ここでは /
)が続いて、セパレータに挟まれた形で正規表現(の文字列)が来る。
上の例ではセパレータに /
を使ったけど、Elixir では全部で8つのセパレータをサポートしている。
内容に応じて使いやすいセパレータを使えばいい。
~s
シギルは文字列を生成する。
iex(4)> ~s(hello) "hello"
~c
は文字リスト。
iex(5)> ~c(hello) 'hello'
そして ~w
は単語のリストを生成する。これは Ruby の %w
と同じだ。
iex(6)> ~w(foo bar baz) ["foo", "bar", "baz"]
Elixir の目標の一つに拡張性がある。シギルも拡張できる。どういうことかというと、標準では提供されていないシギルを自分で定義できるってこと。
初めに例を挙げた ~r は実際には sigil_r 関数の呼び出しになっている。
iex(7)> sigil_r(<<"foo">>, '') ~r/foo/
だから sigil_ナントカ
という関数を定義すればシギルを定義したことになる。例えば整数を返す ~i
を定義してみよう。
iex(8)> defmodule MySigils do ...(8)> def sigil_i(string, []), do: String.to_integer(string) ...(8)> end {:module, MySigils, <<70, 79, 82, 49, 0, 0, 5, 4, 66, 69, 65, 77, 69, 120, 68, 99, 0, 0, 0, 172, 131, 104, 2, 100, 0, 14, 101, 108, 105, 120, 105, 114, 95, 100, 111, 99, 115, 95, 118, 49, 108, 0, 0, 0, 4, 104, 2, ...>>, {:sigil_i, 2}} iex(9)> import MySigils MySigils iex(10)> ~i<42> 42
なるほど、期待した通りに動いている。
ところで sigil って、Perl のシジルと同じスペルなんだけど、どっちの発音が正しい(というか原語に近い)んだろうか。
Elixir には Haskell や Python のようにリスト内包表記がある。
cf. 18 内包表記 – Comprehensions – Elixir
iex(1)> for n <- [1, 2, 3, 4], do: n * n [1, 4, 9, 16]
上の式で n <- [1, 2, 3, 4]
が生成器だ。列挙可能なものなら何でも生成器の右の式に入れられる。
iex(2)> for n <- 1..4, do: n * n [1, 4, 9, 16]
生成器はパターンマッチングに対応していて、パターンにマッチしない値は無視する。次の例では、キーワードリストからキーが :good
の値だけを生成している。
iex(3)> values = [good: 1, good: 2, bad: 3, good: 4] [good: 1, good: 2, bad: 3, good: 4] iex(4)> for {:good, n} <- values, do: n * n [1, 4, 16]
生成器に続けてフィルタを書くと、フィルタを通った値だけが残る。次の例は奇数だけがフィルタを通り、2乗を生成する。
iex(5)> require Integer Integer iex(6)> for n <- 1..4, Integer.is_odd(n), do: n * n [1, 9]
ビットストリングを生成することもできる。
iex(7)> pixels = <<213, 45, 132, 64, 76, 32, 76, 0, 0, 234, 32, 15>> <<213, 45, 132, 64, 76, 32, 76, 0, 0, 234, 32, 15>> iex(8)> for <<r::8, g::8, b::8 <- pixels>>, do: {r, g, b} [{213, 45, 132}, {64, 76, 32}, {76, 0, 0}, {234, 32, 15}]
通常、内包表記はリストを返す。けれど :into
オプションを使うことで、結果を別の型に挿入することができる。次の例は、Bitstring 生成器と :into
オプションを使って、文字列から空白文字を取り除いている。
iex(9)> for <<c <- " hello world ">>, c != ?\s, into: "", do: <<c>> "helloworld"
cf. 17 トライ,キャッチとレスキュー – try, catch and rescue – Elixir
エラーの例は、例えばアトムに数を足そうとするとみることができる。
iex(1)> :atom + 1 ** (ArithmeticError) bad argument in arithmetic expression :erlang.+(:atom, 1)
意図的にランタイムエラーを起こそうというときには、raise/1
マクロを使う。
iex(1)> raise "oops" ** (RuntimeError) oops
その他のエラーは raise/2
マクロにエラーの名前とキーワード引数を渡す。
iex(1)> raise ArgumentError, message: "Invalid argument foo" ** (ArgumentError) Invalid argument foo
モジュールの中で defexception/1
マクロを使うと、自分でエラーを定義することもできる。
iex(1)> defmodule MyError do ...(1)> defexception message: "default message" ...(1)> end {:module, MyError, <<70, 79, 82, 49, 0, 0, 14, 252, 66, 69, 65, 77, 69, 120, 68, 99, 0, 0, 1, 40, 131, 104, 2, 100, 0, 14, 101, 108, 105, 120, 105, 114, 95, 100, 111, 99, 115, 95, 118, 49, 108, 0, 0, 0, 4, 104, 2, ...>>, :ok} iex(2)> raise MyError ** (MyError) default message iex(2)> raise MyError, message: "custom message" ** (MyError) custom message
エラー(例外)は、try
/ rescue
構造で拾うことができる。
iex(2)> try do ...(2)> raise "oops" ...(2)> rescue ...(2)> e in RuntimeError -> e ...(2)> end %RuntimeError{message: "oops"}
上の例ではランタイムエラーを拾って、iex で表示するようにエラー自身を返している。
とはいえ、実際には try
/ rescue
構造を使うことはめったにないそうだ。というのも、例えば File.read/1
関数はファイルを開くとき、失敗したらエラーを起こすのではなく、「成功した」/「失敗した」という情報を含んだタプルを返すからだ。
iex(3)> File.read "hello" {:error, :enoent} iex(4)> File.write "hello", "world" :ok iex(5)> File.read "hello" {:ok, "world"}
ファイルを開くときに起こりうる複数の結果(成功/失敗)に対応するには、単に case
でパターンマッチングすればいい。
Elixir では後で値を捕れるように trhow / catch 構造が用意されている。たとえば、Enum モジュールで 13 の倍数となるような最初の値を見つけたいとしよう。
iex(6)> try do ...(6)> Enum.each -50..50, fn(x) -> ...(6)> if rem(x, 13) == 0, do: throw(x) ...(6)> end ...(6)> "Got nothing" ...(6)> catch ...(6)> x -> "Got #{x}" ...(6)> end "Got -39"
Elixir ではすべてのコードは複数のプロセスの中で動いていてたがいにメッセージをやり取りしている。プロセスが死んだとき、exit
という信号を送る。あるいは exit
信号を送ることで明示的に死ぬこともできる。
iex(7)> spawn_link fn -> exit(1) end ** (EXIT from #PID<0.80.0>) 1 Interactive Elixir (1.3.4) - press Ctrl+C to exit (type h() ENTER for help) iex(1)>
exit
は try
/ catch
構造でキャッチできる。
iex(1)> try do ...(1)> exit "I am exiting" ...(1)> catch ...(1)> :exit, _ -> "not really" ...(1)> end "not really"
try
/ catch
と使うことも珍しいけど、exit
をキャッチするのはもっと珍しいと書いてある。
というのも、プロセスは通常、単にプロセスの exit
信号を待つだけの監視プロセスを備えた監視ツリーにくみこまれて動作していて、監視プロセスが exit
信号を受けとると、監視対象のプロセスは再起動させられるからだ。この監視システムが、エラーの後に「早く失敗する」ことでアプリケーションを既知の初期状態へ戻すことを保証している。らしい。
特定の動作の後、リソースをきれいに片付けられることを保証しなければならない場合、try
/ after
が利用できる例えばファイルを開くとき、必ず閉じることを try
/ after
で保証できる。
iex(2)> {:ok, file} = File.open "sample", [:utf8, :write] {:ok, #PID<0.120.0>} iex(3)> try do ...(3)> IO.write file, "ola" ...(3)> raise "oops, something went wrong" ...(3)> after ...(3)> File.close(file) ...(3)> end ** (RuntimeError) oops, something went wrong
cf. 16 プロトコル – Protocols – Elixir
プロトコルは Elixir で多態を生み出すための仕組みだ。プロトコルの処理は、どんなデータ型であれそのデータ型がプロトコルを実装していれば処理できる。つまりこれが多態ってわけだ。
プロトコルは次のように定義する。
iex(1)> defprotocol Blank do ...(1)> @doc "Returns true if data is considered blank/empty" ...(1)> def blank?(data) ...(1)> end {:module, Blank, <<70, 79, 82, 49, 0, 0, 18, 36, 66, 69, 65, 77, 69, 120, 68, 99, 0, 0, 1, 181, 131, 104, 2, 100, 0, 14, 101, 108, 105, 120, 105, 114, 95, 100, 111, 99, 115, 95, 118, 49, 108, 0, 0, 0, 4, 104, 2, ...>>, {:__protocol__, 1}}
そしてこのプロトコルをデータ型に実装する。
iex(2)> defimpl Blank, for: Integer do ...(2)> def blank?(_), do: false ...(2)> end {:module, Blank.Integer, <<70, 79, 82, 49, 0, 0, 6, 80, 66, 69, 65, 77, 69, 120, 68, 99, 0, 0, 0, 207, 131, 104, 2, 100, 0, 14, 101, 108, 105, 120, 105, 114, 95, 100, 111, 99, 115, 95, 118, 49, 108, 0, 0, 0, 4, 104, 2, ...>>, {:__impl__, 1}} iex(3)> defimpl Blank, for: List do ...(3)> def blank?([]), do: true ...(3)> def blank?(_), do: false ...(3)> end {:module, Blank.List, <<70, 79, 82, 49, 0, 0, 6, 100, 66, 69, 65, 77, 69, 120, 68, 99, 0, 0, 0, 211, 131, 104, 2, 100, 0, 14, 101, 108, 105, 120, 105, 114, 95, 100, 111, 99, 115, 95, 118, 49, 108, 0, 0, 0, 4, 104, 2, ...>>, {:__impl__, 1}} iex(4)> defimpl Blank, for: Map do ...(4)> def blank?(map), do: map_size(map) == 0 ...(4)> end {:module, Blank.Map, <<70, 79, 82, 49, 0, 0, 6, 140, 66, 69, 65, 77, 69, 120, 68, 99, 0, 0, 0, 207, 131, 104, 2, 100, 0, 14, 101, 108, 105, 120, 105, 114, 95, 100, 111, 99, 115, 95, 118, 49, 108, 0, 0, 0, 4, 104, 2, ...>>, {:__impl__, 1}} iex(5)> defimpl Blank, for: Atom do ...(5)> def blank?(false), do: true ...(5)> def blank?(nil), do: true ...(5)> def blank?(_), do: false ...(5)> end {:module, Blank.Atom, <<70, 79, 82, 49, 0, 0, 6, 124, 66, 69, 65, 77, 69, 120, 68, 99, 0, 0, 0, 211, 131, 104, 2, 100, 0, 14, 101, 108, 105, 120, 105, 114, 95, 100, 111, 99, 115, 95, 118, 49, 108, 0, 0, 0, 4, 104, 2, ...>>, {:__impl__, 1}}
いま、Integer
、List
、Map
、Atom
について Blank プロトコルを実装した。必要であればほかの型にも実装する。
それじゃ、確かめてみよう。
iex(6)> Blank.blank?(0) false iex(7)> Blank.blank?([]) true iex(8)> Blank.blank?([1, 2, 3]) false
文字列に対しては実装していないのでエラーになる。
iex(9)> Blank.blank?("hello") ** (Protocol.UndefinedError) protocol Blank not implemented for "hello" iex:1: Blank.impl_for!/1 iex:3: Blank.blank?/1
構造体はマップの拡張だけど、プロトコルの実装は共有していない。確かめるために、User
構造体を定義してみる。
iex(9)> defmodule User do ...(9)> defstruct name: "john", age: 27 ...(9)> end {:module, User, <<70, 79, 82, 49, 0, 0, 8, 240, 66, 69, 65, 77, 69, 120, 68, 99, 0, 0, 0, 186, 131, 104, 2, 100, 0, 14, 101, 108, 105, 120, 105, 114, 95, 100, 111, 99, 115, 95, 118, 49, 108, 0, 0, 0, 4, 104, 2, ...>>, %User{age: 27, name: "john"}}
そして確かめる。
iex(10)> Blank.blank?(%{}) true iex(11)> Blank.blank?(%User{}) ** (Protocol.UndefinedError) protocol Blank not implemented for %User{age: 27, name: "john"} iex:1: Blank.impl_for!/1 iex:3: Blank.blank?/1
(空の)マップに対しては true
が返ってきているのに対して、User
ではエラーになっている。エラーを避けるためには、User
構造体に対して Blank
プロトコルを実装する必要がある(とはいえ、どういうときに true
にするかわからないけど。この例ではすべて false
かな)。
すべての型にプロトコルを実装するのは面倒なので、デフォルトの実装を与えておく方法がある。プロトコル定義の中で@fallback_to_any
を true
に設定すると可能になる。
iex(11)> Blank.blank?(%User{}) ** (Protocol.UndefinedError) protocol Blank not implemented for %User{age: 27, name: "john"} iex:1: Blank.impl_for!/1 iex:3: Blank.blank?/1 iex(11)> defprotocol Blank do ...(11)> @fallback_to_any true ...(11)> def blank?(data) ...(11)> end warning: redefining module Blank (current version defined in memory) iex:11 {:module, Blank, <<70, 79, 82, 49, 0, 0, 18, 40, 66, 69, 65, 77, 69, 120, 68, 99, 0, 0, 1, 136, 131, 104, 2, 100, 0, 14, 101, 108, 105, 120, 105, 114, 95, 100, 111, 99, 115, 95, 118, 49, 108, 0, 0, 0, 4, 104, 2, ...>>, {:__protocol__, 1}}
あー、なんか Blank モジュールを再定義したんで warning が出てるな。大丈夫かな?とりあえず進めてみる。
とにかく、上のようにプロトコルを定義して、Any に対して実装すればいい。
iex(12)> defimpl Blank, for: Any do ...(12)> def blank?(_), do: false ...(12)> end {:module, Blank.Any, <<70, 79, 82, 49, 0, 0, 6, 68, 66, 69, 65, 77, 69, 120, 68, 99, 0, 0, 0, 207, 131, 104, 2, 100, 0, 14, 101, 108, 105, 120, 105, 114, 95, 100, 111, 99, 115, 95, 118, 49, 108, 0, 0, 0, 4, 104, 2, ...>>, {:__impl__, 1}}
これで、プロトコルを明示的に実装していないすべての型(構造体を含む)は、Blank.blank?
に対して false
を返すようになった。
iex(13)> Blank.blank?(%User{}) false
Elixir には組み込みのプロトコルがある。例えば Enumerable
。これを実装しているデータ型には Enum
モジュールの関数が使える。
また、String.Chars
。to_string/1
が様々な型に適用できるのはこのプロとるを実装しているからだ。
ほかにもあるらしいけど、とりあえずこのへんで。
去年の12月に2つの文字列のレーベンシュタイン距離を求めるっていうのを、JavaScript と Haskell でやった。もともとは Python での実装を参考に2次元配列を使ってやってみたもので、JavaScript版はともかく Haskell版は2次元配列の更新に苦労した。
それが、今日ふと1次元配列でやってみたらどうだろうと思いついた。
つまりこうだ。(m + 1)行×(n + 1)列(m、n は比較する文字列2つの長さ)の各行をつなげた1次元配列を考えて、2次元配列の時の座標を表すタプル (i, j) で初期化しておく。0 < i, 0 < j のとき、(i, j) の値はひとつ左(i-1, j)、ひとつ上(i, j-1)、左上(i-1, j-1)から決まるから、これを1次元配列のインデックスに直すと次のようになる:
これをコードに落としこんでやるとこうなった:
module Main where import System.Environment (getArgs) levenshteinDistance :: String -> String -> Int levenshteinDistance s1 s2 = last ld where ld = map f [(x, y) | x <- [0..m], y <- [0..n]] m = length s1 n = length s2 f (0, 0) = 0 f (i, 0) = i f (0, j) = j f (i, j) = minimum [a, b, c] where a = ld !! (i * (n + 1) + j - 1) + 1 b = ld !! ((i - 1) * (n + 1) + j) + 1 c = ld !! ((i - 1) * (n + 1) + j - 1) + c' c' = if s1 !! (i - 1) == s2 !! (j - 1) then 0 else 1 main :: IO () main = do args <- getArgs let s1 = args !! 0 let s2 = args !! 1 print $ levenshteinDistance s1 s2
結果はこうだ。
takatoh@apostrophe $ runhaskell ld2.hs apple play 4 takatoh@apostrophe $ runhaskell ld2.hs perl pearl 1
OK、うまくいった。
だけど、上のコードはまだ2次元配列の意識を引きずっている。もっと単純にできるはずだ。1次元配列のインデックスを x とすると:
となる。これで一般部については2次元配列を気にしなくて良くなった。ただし問題がある。いちばん上の行(第0行)といちばん左の列(第0列)だ。少し考えて、x を (n + 1) で割った商と余りを使えばいいと気がついた。コードにするとこう:
module Main where import System.Environment (getArgs) levenshteinDistance :: String -> String -> Int levenshteinDistance s1 s2 = last ld where ld = map f [0..((m + 1) * (n + 1) -1)] m = length s1 n = length s2 f x | x < n + 1 = x | x `rem` (n + 1) == 0 = x `div` (n + 1) | otherwise = minimum [a, b, c] where a = ld !! (x - 1) + 1 b = ld !! (x - (n + 1)) + 1 c = ld !! (x - (n + 1) - 1) + c' c' = if s1 !! i == s2 !! j then 0 else 1 i = x `div` (n + 1) - 1 j = x `rem` (n + 1) - 1 main :: IO () main = do args <- getArgs let s1 = args !! 0 let s2 = args !! 1 print $ levenshteinDistance s1 s2
1行だけだけど長くなってしまった。だけど考え方はシンプルのように思う。実行してみよう。
takatoh@apostrophe $ runhaskell ld3.hs apple play 4 takatoh@apostrophe $ runhaskell ld3.hs perl pearl 1
OK。
構造体はマップの拡張だ。初期値、コンパイル時の保証、多態をもたらす、って書いてある。多態ってのはどういう意味だ?それから初期値があるのもなんだかなぁという気がする。
まあ、いい。例を見てみよう。構造体を定義するには、モジュールの中で defstruct/1
を使う。
defmodule User do defstruct name: "john", age: 27 end
^o^ > iex struct.exs Eshell V8.0 (abort with ^G) Interactive Elixir (1.3.4) - press Ctrl+C to exit (type h() ENTER for help) iex(1)> %User{} %User{age: 27, name: "john"} iex(2)> %User{name: "meg"} %User{age: 27, name: "meg"} iex(3)> is_map(%User{}) true
構造体は、用意しているフィールドが存在することをコンパイル時に保証する。逆に言うと、用意していないフィールドを使おうとするとエラーになる。
iex(4)> %User{oops: :field} ** (KeyError) key :oops not found in: %User{age: 27, name: "john"} (stdlib) :maps.update(:oops, :field, %User{age: 27, name: "john"}) struct.exs:3: anonymous fn/2 in User.__struct__/1 (elixir) lib/enum.ex:1623: Enum."-reduce/3-lists^foldl/2-0-"/3 expanding struct: User.__struct__/1 iex:4: (file)
構造体はマップの拡張なので、マップと同様のアクセスができる。
iex(4)> john = %User{} %User{age: 27, name: "john"} iex(5)> john.name "john" iex(6)> meg = %{john | name: "meg"} %User{age: 27, name: "meg"} iex(7)> %{meg | oops: :field} ** (KeyError) key :oops not found in: %User{age: 27, name: "meg"} (stdlib) :maps.update(:oops, :field, %User{age: 27, name: "meg"}) (stdlib) erl_eval.erl:255: anonymous fn/2 in :erl_eval.expr/5 (stdlib) lists.erl:1263: :lists.foldl/3
パターンマッチングでもよく用いられる。
iex(7)> %User{name: name} = john %User{age: 27, name: "john"} iex(8)> name "john" iex(9)> %User{} = %{} ** (MatchError) no match of right hand side value: %{}
構造体はディクショナリではないので Dict
モジュールからは使えない。
iex(9)> Dict.get(%User{}, :name) ** (UndefinedFunctionError) function User.get/3 is undefined or private User.get(%User{age: 27, name: "john"}, :name, nil)