ふるつき

私は素直に思ったことを書いてるけど、上から目線だって言われる

言語処理100本ノック2015 をD言語でやる【第1章】

 自然言語処理別に興味あるわけじゃないんですが、 yamasy がこれをやっててなかなか楽しそうじゃんと思って練習がてらです。

yamasy1549.hateblo.jp

私はD言語でやりたいのでD言語でやります。 リポジトリは これ。

github.com

00. 文字列の逆順

char[] answer() {
    import std.algorithm;
    char[] str = "stressed".dup;
    reverse(str);
    return str;
}

D言語だと、 BidirectionalRangeRandomAccessRange を反転できる reverse 関数が algorithm パッケージにあります。これは受け取った Range を変更するものなので、引数は mutable である必要がある。個人的には新しいRangeを作って返してくれればいいのでは……と思いますが。とか書きつつ調べたら、遅延評価で逆順に辿ってくれる retrorange パッケージに用意されていました。こちらを使って書くとこうですね。

string answer() {
    import std.range, std.array, std.conv;
    return "stressed".retro.array.to!string;
}

retro の返す型は Result みたいな独自の型なので array で配列に直してあげるとか、 D言語では文字列を Range として扱うときは dchar の Range になるから tostring に変換してやってるとかそういうイディオムが登場してます。D言語でこういう文字列を扱うとき、どういう型で受取り、どういう型で処理をして、どういう型を返せばいいのかわからないです(この例でも char[]string は別の型なので……)。

01. 「パタトクカシーー」

char[] answer() {
    import std.range;
    import std.array;
    import std.conv;
    dchar[] patatokukasy = "パタトクカシーー".to!(dchar[]);
    return patatokukasy[1..$].stride(2).array().to!(char[]);
}

日本語を文字単位で扱うために dchar の配列にしてます。 dcharUTF-32 のcode unit一つに対応していて、今の所 UTF-32 なら日本語一文字を 固定長で表せます(Code Unitってなに)。なんか rune みたいな型ほしいですよね(これはよしなにしたいという意味)。

それでですが、奇数インデックスを持つ要素だけを抽出したいので range.stride を使いました。これは Range (すくなくとも InputRange なのでわりとどんなRangeでも) を n-1 個飛ばしで辿るRangeにしてくれる関数です。 スライスって言うのかわかりませんが、配列の範囲を [1..$] ($はアクセサの中では「配列長」になる)にして先頭一文字を抜いた配列を対象にしてるので「タ」から1文字置きに要素をたどれます。Rangeが返ってきているので array で配列に変換してやり、 dchar の配列から char の配列に変換しておきます。なんとなく、インターフェースとしては char[] を扱うのがいいと思っているので。もしかしたら普通にRangeを返すのが関数としては綺麗なのかもですね。わからん

ところで、 Rangeの先頭を落としたRangeを返す dropOne という関数もあったので patatokakusy[1..$]patatokakusy.dropOne() でいいですね。

02. 「パトカー」+「タクシー」=「パタトクカシーー」

char[] answer() {
    import std.range;
    import std.conv;

    string str1 = "パトカー";
    string str2 = "タクシー";

    dchar[] result;

    foreach (c1, c2; zip(str1, str2)) {
        result ~= c1;
        result ~= c2;
    }

    return result.to!(char[]);
}

こういうのは見た瞬間に 「 zip だ!」ってなりますね。今回は foreach で書きましたが zip.map.join でもよかったかも。

03. 円周率

size_t[] answer() {
    import std.array;
    import std.algorithm;
    import std.uni;

    string given = "Now I need a drink, alcoholic of course, after the heavy lectures involving quantum mechanics.";
    return given.split(" ").map!(w => w.count!(isAlpha)).array;
}

単語に分割して単語ごとの isAlpha にマッチする文字数を数えています。 D言語の関数の喚び出し方は基本的に func!(template arguments)(arguments) で、必要ない () は省略できる && 関数の第一引数はレシーバのようにかけるので(f(a,b)a.f(b) とかいていいし、 引数bを取らないなら a.f でいい)、こんな呼び方になってます。

04. 元素記号

string[size_t] answer() {
    import std.array;
    import std.algorithm : canFind;

    string given = "Hi He Lied Because Boron Could Not Oxidize Fluorine. New Nations Might Also Sign Peace Security Clause. Arthur King Can.";

    auto idxs = [1, 5, 6, 7, 8, 9, 15, 16, 19];
    string[size_t] assoc;
    foreach (i, v; given.split(" ")) {
        if (idxs.canFind(i+1)) {
            assoc[i] = v[0..1];
        }
        else {
            assoc[i] = v[0..2];
        }
    }
    return assoc;
}

string[size_t]size_t をキー、 string を値としてもつ連想配列の型です。今回のこれはかなりそのままな実装ですね。

05. n-gram

import std.traits;
import std.range;

auto n_gram(Range)(Range r, size_t n)
if (isInputRange!(Unqual!Range))  // unqaulはqualifierを外すのでconst(int) -> int ということ
in
{
    assert(n >= 1);
}
do
{
    // とりあえずInputRangeということで
    // front, popFront, empty を持ってる
    static struct Result {
 private:
        alias R = Unqual!Range;
        alias T = ElementType!R;
        size_t n;
        T[] buf;
 public:
        R source;

        this(R source, size_t n) {
            this.source = source;
            this.n = n;
            this.buf = [];
            foreach (i; 0..n) {
                this.buf ~= this.source.front();
                this.source.popFront();
            }
        }

        @property {
            bool empty() {
                return this.buf.length < this.n;
            }
            auto ref front() {
                return this.buf;
            }
        }
        void popFront() {
            this.buf.popFront();
            if (!this.source.empty) {
                this.buf ~= this.source.front();
                this.source.popFront();
            }
        }
    }

    return Result(r, n);
}

auto answer1() {
    import std.array;
    string str = "I am an NLPer";
    return n_gram(str.split(" "), 2);
}
auto answer2() {
    import std.array;
    string str = "I am an NLPer";
    return n_gram(str.split(""), 2);
}

うえぇぇ。読みたくない。D言語のRange、実装すると結構読みづらい感じになりますよね。インターフェースは良いんだけど。 関数のボディの前にある if は引数の型に制約を与えるものです。 in は関数呼び出し時の事前条件で、 do のあとのブロックが関数本体です。 body じゃなくなったんですかね。

n_gram 関数でやってるのは新しい構造体を作って返すだけ。です。Rangeを返す関数の実装はだいたいこんな感じになってて、これのせいで返り型が auto になってる関数が標準ライブラリに散見されます。

@property bool empty();@property T front();void popFront() を持つ構造体は InputRange インターフェース(インターフェースというと嘘)を満たしています(この辺Interfaceじゃなくてコンセプト(?)で型を縛ってるのなんでですか)。なんとなくRangeを作りたくなったのでつくった。

06. 集合

import std.stdio;
import ngram;

void main()
{
    import std.array;
    import std.algorithm;

    auto X = "paraparaparadise".n_gram(2).array().sort!"a < b"().uniq().array();
    auto Y = "paragraph".n_gram(2).array().sort!"a < b"().uniq().array();

    writeln("X: ", X);
    writeln("Y: ", Y);
    
    writeln("Union: ", multiwayUnion([X, Y]));
    writeln("Intersection: ", setIntersection(X, Y));
    writeln("Difference: ", setDifference(X, Y));


}

D言語にはsetがない のですが、配列を set のように扱う関数がいくつか用意されています。 sortのテンプレート引数がキモいですが、 sort!((a,b) => a < b) と同じです

07. テンプレートによる文生成

char[] func(X, Y, Z)(X x, Y y, Z z) {
    import std.format;
    return "%s時の%sは%s".format(x, y, z).dup;
}

char[] answer() {
    return func(12, "気温", 22.4);
}

やるだけみたいなところがある。 X, Y, Z はテンプレート引数です。

08. 暗号文

string cipher(string src)
{
    import std.algorithm;
    import std.array;
    import std.uni;

    return src.map!((dchar c) => (c.isAlpha && c.isLower) ? cast(char)(219 - c) : cast(char)c).array;
}

この頃になると入力と出力を string にしてますね。これ、「c」みたいな文字にも反応しそうで怖いなぁ。そのまえに char に入らなくて死ぬか。

09. Typoglycemia

string typoglycemia(string str)
{
    import std.algorithm;
    import std.array;
    import std.random;
    import std.string;

    auto rnd = rndGen();

    return str.split(" ").map!((string word) {
        if (word.length <= 4) { return word; }
        ubyte[] sore = word[1..$-1].representation.dup;
        randomShuffle(sore, rnd);
        return cast(string)(word[0] ~ sore.assumeUTF ~ word[$-1]);
    }).array.join(" ");
}

気合を入れると読めます。そんなに難しくはない……はず。


D言語で文字列を扱う関数のインターフェースどうしたらいいのかわからない……。そこかしこで array 使ってるのめっちゃ罪悪感ある……