行き詰った

去年の暮れに、今年は 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のサムネイルを作るときの注意

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]

Bitstring生成器

ビットストリングを生成することもできる。

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

通常、内包表記はリストを返す。けれど :into オプションを使うことで、結果を別の型に挿入することができる。次の例は、Bitstring 生成器と :into オプションを使って、文字列から空白文字を取り除いている。

iex(9)> for <<c <- " hello world ">>, c != ?\s, into: "", do: <<c>>
"helloworld"

try, catch, rescue

 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

エラー(例外)は、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 でパターンマッチングすればいい。

throw / catch

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"

exit

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)>

exittry / 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 信号を受けとると、監視対象のプロセスは再起動させられるからだ。この監視システムが、エラーの後に「早く失敗する」ことでアプリケーションを既知の初期状態へ戻すことを保証している。らしい。

after

特定の動作の後、リソースをきれいに片付けられることを保証しなければならない場合、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}}

いま、IntegerListMapAtom について 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_anytrue に設定すると可能になる。

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.Charsto_string/1 が様々な型に適用できるのはこのプロとるを実装しているからだ。
ほかにもあるらしいけど、とりあえずこのへんで。

文字列間のレーベンシュタイン距離を求める(3)Haskell版ふたたび

去年の12月に2つの文字列のレーベンシュタイン距離を求めるっていうのを、JavaScriptHaskell でやった。もともとは 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次元配列のインデックスに直すと次のようになる:

  • ひとつ左: i * (n + 1) + j – 1
  • ひとつ上: (i – 1) * (n + 1) + j
  • 左上: (i – 1) * (n + 1) + j – 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 とすると:

  • ひとつ左: x – 1
  • ひとつ上: x – (n + 1)
  • 左上: x – (n + 1) – 1

となる。これで一般部については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。

構造体

cf. 15 構造体 – Structs – Elixir

構造体はマップの拡張だ。初期値、コンパイル時の保証、多態をもたらす、って書いてある。多態ってのはどういう意味だ?それから初期値があるのもなんだかなぁという気がする。
まあ、いい。例を見てみよう。構造体を定義するには、モジュールの中で 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)