Help us understand the problem. What is going on with this article?

D言語で形態素解析

More than 5 years have passed since last update.

D言語 Advent Calendar 2014 に空きがあったので書かせて頂きました。
雑文で申し訳ないです。

和布蕪

さてさて、皆さん、和布蕪は好きですか?
私は大好きです。

そんなわけでMeCabをDから使うという話です。

MeCabとは

MeCabとは、工藤拓 氏によって開発された形態素解析エンジンです。
オープンソース・ソフトウェアとしてGPLとかLGPLとかBSDライセンスとかで公開されているようです。
こんな感じで使います。

$ echo "失敗したっていいじゃない、にんげんだもの" | mecab
失敗  名詞,サ変接続,*,*,*,*,失敗,シッパイ,シッパイ
し 動詞,自立,*,*,サ変・スル,連用形,する,シ,シ
た 助動詞,*,*,*,特殊・タ,基本形,た,タ,タ
って  助詞,格助詞,連語,*,*,*,って,ッテ,ッテ
いい  形容詞,自立,*,*,形容詞・イイ,基本形,いい,イイ,イイ
じゃ  助詞,副助詞,*,*,*,*,じゃ,ジャ,ジャ
ない  助動詞,*,*,*,特殊・ナイ,基本形,ない,ナイ,ナイ
、 記号,読点,*,*,*,*,、,、,、
にんげんだもの   名詞,固有名詞,一般,*,*,*,にんげんだもの,ニンゲンダモノ,ニンゲンダモノ
EOS

……MeCabさん的には(というかIPA辞書的には)「にんげんだもの」は固有名詞のようです。
ドラマのタイトルだから? まぁいいや。
詳しくはwikipedia公式サイトを参照して下さい。

形態素解析とは

自然言語で書かれた文を、形態素に分解して、それぞれの品詞を判別する事です。
こちらも詳しくはwikipedia辺りをご参照ください。
(だって詳しく説明できる程の知識を持ってないんだもの)

MeCabをDから使う

さて、いよいよ本題です。このMeCabをD言語で使ってみよう、という話です。

MeCabとC++とD

MeCabはC++で書かれているようです。
残念な事に、D言語のバイナリはC++のバイナリとの互換性があまりありません。
しかし「あまりありません」という事は、全く無いというわけでもないのです。
公式サイトの当該のページによると、クラスや仮想関数なんかはある程度互換性があるようです。
ソースコードに入っている解説によると、MeCabの基本的な機能はMeCab::Taggerクラスさえあれば使えるようです。
……何とかDから使えるんじゃないでしょうか?

環境

Ubuntu 14.04 の上で動かしています。
既にMeCab自体は apt-get で入れてある状態です。
なので、Windows とかだと上手く行かないかもしれません。
辞書はUTF-8のIPA辞書です。

DからC++のクラスを呼ぶ

上記サイトの解説によると、DからC++のクラスを使うには、Dのinterfaceを使えば良いようです。
というわけで、そのように書いてみましょう。

test_cpp.d
extern(C++, MeCab)
{
    interface Tagger
    {
        const(char)* parse(const(char)*);
    }

    Tagger createTagger(const(char)*);
}

import std.string : toStringz;
import std.conv : to;
import std.stdio;

void main()
{
    auto tagger = createTagger("");
    auto input = "太郎は次郎が持っている本を花子に渡した。";
    auto result = tagger.parse(input.toStringz);

    writeln(result.to!string);
}

何だかとっても適当に見えるかもしれませんが、ご容赦を。
動けば良いのです。
というわけで動かしてみましょう。

$ dmd test_cpp.d -L-lmecab

リンカに MeCab のライブラリ(libmecab.a)をリンクするよう言うのを忘れないで下さい。

$ ./test_cpp
Segmentation fault (コアダンプ)

すいません、動きませんでした
ま、まぁプログラミングには失敗が付き物ですからね。
コアダンプを吐いたようなので、これを調べてみましょう……あ、設定するの忘れてたから出力されてないや。
しかしどうせデバッグ情報を入れてないのでまぁ、ね。
今度は -g オプションを付けてコンパイルしなおしてみましょう。

$ dmd -g test_cpp.d -L-lmecab

それで、GDBを使ってデバッグを……

$ gdb ./test_cpp

    :
    :

Program received signal SIGSEGV, Segmentation fault.
0xb7e09d2a in MeCab::Viterbi::analyze(MeCab::Lattice*) const ()
   from /usr/lib/libmecab.so.2
(gdb) where
#0  0xb7e09d2a in MeCab::Viterbi::analyze(MeCab::Lattice*) const ()
   from /usr/lib/libmecab.so.2
#1  0xb7e0c821 in ?? () from /usr/lib/libmecab.so.2
#2  0x0806fc1a in D main () at test_cpp.d:16
#3  0x08074ada in rt.dmain2._d_run_main() ()
#4  0x08074a50 in rt.dmain2._d_run_main() ()
#5  0x08074a9f in rt.dmain2._d_run_main() ()
#6  0x08074a50 in rt.dmain2._d_run_main() ()
#7  0x080749e7 in _d_run_main ()
#8  0x0807256c in main ()

んん……?
……。
…。

その後、色々調べてみたのですがどうにも原因がわかりませんでした。
やはり私にはDとC++の接続などという高尚な行為は早すぎたのでしょうか。
どなたか解る方、ご教授くだされば大変喜びます。

終わり

(12月24日追記)
コメントでご教授頂きました!
記事の末尾に原因と解決策を追記しています。

それで

これで終わると流石に「真面目に書け」とお叱りを受けそうなので、もう少し続けましょう。
MeCabにはCのAPIも用意されています。
RubyやPythonやJavaへのポーティングは、このCのAPI(とSWIG)を利用して行われているようです。

MeCabとCとD

D言語はC言語との接続がなるべく簡単なように設計されているらしいです。
なので、CのAPIを通してならDからも、もうちょっと簡単に呼べるのではないでしょうか?
というか最初からそうしておけよ、という感じなのですが、C++をラップしたCをラップしたD、よりもC++をラップしたD、の方が素直で良いなぁ、なんて思ってしまったのが間違いでした。
それでは、1つのコードには1024ワード分の価値があるらしいので、早速書いてみましょう。

test_c.d
extern(C)
{
    struct mecab_t;

    mecab_t* mecab_new2(const(char)* arg);
    void mecab_destroy(mecab_t *mecab);
    const(char)* mecab_sparse_tostr(mecab_t *mecab, const(char)* str);
}

import std.string : toStringz;
import std.conv : to;
import std.stdio;

void main()
{
    auto input = "太郎は次郎が持っている本を花子に渡した。";

    auto mecab = mecab_new2("");
    scope(exit) mecab_destroy(mecab);

    auto result = mecab_sparse_tostr(mecab, input.toStringz);
    writefln(result.to!string);
}

ちょっとしたデジャヴ感がありますか?
まぁ上のと似たり寄ったりな内容になりますよね。
……先程の失敗を思い出して心が苦しいですが、邪念は振り払って、コンパイルしてみましょう。

$ dmd test_c.d -L-lmecab

ライブラリとのリンクを(ry

$ ./test_c
太郎  名詞,固有名詞,人名,名,*,*,太郎,タロウ,タロー
は 助詞,係助詞,*,*,*,*,は,ハ,ワ
次郎  名詞,固有名詞,人名,名,*,*,次郎,ジロウ,ジロー
が 助詞,格助詞,一般,*,*,*,が,ガ,ガ
持っ  動詞,自立,*,*,五段・タ行,連用タ接続,持つ,モッ,モッ
て 助詞,接続助詞,*,*,*,*,て,テ,テ
いる  動詞,非自立,*,*,一段,基本形,いる,イル,イル
本 名詞,一般,*,*,*,*,本,ホン,ホン
を 助詞,格助詞,一般,*,*,*,を,ヲ,ヲ
花 名詞,一般,*,*,*,*,花,ハナ,ハナ
子 名詞,接尾,一般,*,*,*,子,コ,コ
に 助詞,格助詞,一般,*,*,*,に,ニ,ニ
渡し  動詞,自立,*,*,五段・サ行,連用形,渡す,ワタシ,ワタシ
た 助動詞,*,*,*,特殊・タ,基本形,た,タ,タ
。 記号,句点,*,*,*,*,。,。,。
EOS

やった! 動いた!

解説

上の例で使用したmecab_sparse_tostr()関数は、文を形態素解析して、その結果を文字列にして返してくれるという関数です。
人間が見る分にはとても解りやすいですが、計算機にとってはそうではないかもしれません。
というわけで、プログラムの中で形態素解析の結果を利用するには、他の関数を利用するのが良いでしょう。

ノードに分割

そこで出てくるのがmecab_sparse_tonode()関数です。
この関数は、文を形態素毎に分割して、その形態素の情報を格納した構造体を作り、それらの構造体を双方向リストとして繋げ、その先頭の構造体のポインタを返してくれる関数です。……文章にするとやたらややこしいですが、要するに分析結果を形態素毎に扱えるようになるという事です。
今度はこの関数を使って形態素解析をしてみましょう。しかしmecab_sparse_tonode()関数を使って各形態素を調べるには、それが返す構造体であるmecab_node_tの定義が無ければなりません。他にもmecab_dictionary_info_tとかも使いたいなぁ、と考えていくと、全てのソースを1つのファイルに書いていくのはちょっと問題が大きいという事に思い至ります。
そういうわけで、ソースファイルは分割しましょう。

mecab.di
module mecab;

__gshared extern(C) nothrow @nogc:

enum
    MECAB_NOR_NODE = 0,
    MECAB_UNK_NODE = 1,
    MECAB_BOS_NODE = 2,
    MECAB_EOS_NODE = 3,
    MECAB_EON_NODE = 4;

struct mecab_t;

struct mecab_dictionary_info_t {
    const(char)* filename;
    const(char)* charset;
    uint size;
    int type;
    uint lsize;
    uint rsize;
    ushort version_;
    mecab_dictionary_info_t *next;
}

struct mecab_path_t {
    mecab_node_t* rnode;
    mecab_path_t* rnext;
    mecab_node_t* lnode;
    mecab_path_t* lnext;
    int cost;
    float prob;
}

struct mecab_node_t {
    mecab_node_t* prev;
    mecab_node_t* next;
    mecab_node_t* enext;
    mecab_node_t* bnext;
    mecab_path_t* rpath;
    mecab_path_t* lpath;
    const(char)* surface;
    const(char)* feature;
    uint id;
    ushort length;
    ushort rlength;
    ushort rcAttr;
    ushort lcAttr;
    ushort posid;
    ubyte char_type;
    ubyte stat;
    ubyte isbest;
    float alpha;
    float beta;
    float prob;
    short wcost;
    long cost;
}

mecab_t* mecab_new2(const(char)*);
void mecab_destroy(mecab_t*);

const(char)* mecab_sparse_tostr(mecab_t*, const(char)*);

const(mecab_node_t)* mecab_sparse_tonode(mecab_t*, const(char)*);

const(mecab_dictionary_info_t)* mecab_dictionary_info(mecab_t*);

(12月24日追記)
コメントでご指摘を頂きました。
それに従い、ソースコードの重大な問題点を修正しました。
記事の末尾に追記してあります。

……CのヘッダをDに翻訳するのに慣れてる人から見ると、突っ込みどころ満載かもしれません。
これはおかしい、みたいな点があれば、ご指摘いただけると助かります。
兎も角も、これでMeCabの関数を使う準備は整いました。次は本体を書くだけです。

test.d
import mecab;
import std.stdio : writefln;
import std.string : toStringz;
import std.conv : to;

void main(string[] argv) {
    auto input = argv.length > 1 ? argv[1] : "太郎は次郎が持っている本を花子に渡した。";

    auto mecab = mecab_new2("");
    scope(exit) mecab_destroy(mecab);

    auto node = mecab_sparse_tonode(mecab, input.toStringz);
    for(; node; node = node.next) {
        if (node.stat == MECAB_NOR_NODE || node.stat == MECAB_UNK_NODE) {
            writefln("%s\t%s", node.surface[0 .. node.length].to!string, node.feature.to!string);
        }
    }
    auto dic = mecab_dictionary_info(mecab);

    writefln("filename: %s\ncharset: %s\nsize: %s\ntype: %s\n",
             dic.filename.to!string,
             dic.charset.to!string,
             dic.size,
             dic.type);
}

既にお気付きの方も多いでしょうが、今までのコードは上記のMeCabのソースに入っているサンプルから一部抜き出して、D風に書きなおしただけです。
見ての通り、mecab_node_tのメンバ変数であるsurfaceだとかlengthだとかfeatureだとかを利用しています。surfaceは形態素の文字列のポインタで、lengthがその長さです。featureには形態素の情報が文字列として入っています。
その下のmecab_dictionary_info()は、利用する辞書の情報を得る関数です。MeCabの辞書は、mecabrcファイルの中に書かれているものを使うのだと思います。読み込んだ辞書の情報を取り出して表示してみます。
これを動かすと、以下のようになります。

$ ./test "D は C言語風の構文を持つ静的型付け言語です。"
D   名詞,固有名詞,組織,*,*,*,*
は 助詞,係助詞,*,*,*,*,は,ハ,ワ
C   名詞,固有名詞,組織,*,*,*,*
言語  名詞,一般,*,*,*,*,言語,ゲンゴ,ゲンゴ
風 名詞,接尾,一般,*,*,*,風,フウ,フー
の 助詞,連体化,*,*,*,*,の,ノ,ノ
構文  名詞,一般,*,*,*,*,構文,コウブン,コーブン
を 助詞,格助詞,一般,*,*,*,を,ヲ,ヲ
持つ  動詞,自立,*,*,五段・タ行,基本形,持つ,モツ,モツ
静的  名詞,形容動詞語幹,*,*,*,*,静的,セイテキ,セイテキ
型付け   名詞,一般,*,*,*,*,型付け,カタツケ,カタツケ
言語  名詞,一般,*,*,*,*,言語,ゲンゴ,ゲンゴ
です  助動詞,*,*,*,特殊・デス,基本形,です,デス,デス
。 記号,句点,*,*,*,*,。,。,。
filename: /var/lib/mecab/dic/debian/sys.dic
charset: UTF-8
size: 392126
type: 0

mecab_node_tをforループ回して使っている部分は、結果としてmecab_sparse_tostr()関数と似たような出力をしていますね。その下は、辞書に関する情報です。
解説するような事は、特に、無いです。

もう少し高度な事をする

さて、これだけだとmecab_sparse_tostr()関数がやった事を自分で実装しただけなので、mecab_node_tの情報をまるで活用できていませんね。これは良くない。
なので、もう少しややこしい事をしてみたいです。

ところで、「プログラミング言語D」はご存知ですよね。日本で2番目に出たD言語の本です(多分)。
そういえば日本で最初のD言語の本が出てから10年経ったらしいですね。
閑話休題。それで、「プログラミング言語D」の第1章には、『ハムレット』を分析する例が出ています。
それにあやかって、日本の古典的名劇作家である世阿弥の謡曲の分析を……と思ったのですが、流石に室町時代の文章をMeCabに読ませるのは難しいかもしれません。
そこで、少し時代を下って、明治の文豪の文章をMeCabで分析してみましょう。

小説の文章を分析

分析といっても、単に小説の文章を形態素解析して、その結果を表示するだけでは面白くない。というか恐らく、長さにもよりますが、コンソールがあまり読む気のしない状態になる事請け合いです。折角プログラム中で文章を形態素に分解しているのですから、それを利用してみましょう。
文章というのは、名詞や、動詞や、形容詞等の品詞から成り立っています。MeCabでは、それぞれの形態素がそのどれに当てはまるのか、推測してくれます。
文章の特色には、様々な要素がありますが、これらの品詞の割合が関わってくるのは間違いないと思われます。簡素な文体と絢爛豪華な文体の対比は、それこそ古代ローマ時代のカエサルとカトーの比較に見られるように、古くから言われている事です。
文章から、それぞれの品詞がどれだけ使われているのか、またその文章内での割合はどの程度なのかを計算する事ができれば、どういう文体が良い文体でどういう文体が悪い文体なのかを機械的に計算する一助になるかもしれません。
ただあんまりややこしいものを作ってもアレですので、調べるのは「名詞、形容詞、動詞、副詞、接続詞」の5つだけに限定して、それらの中にある細かい違い(自立語か接尾語か、一般名詞か固有名詞か、等)は無視する事にしましょう。

と、卒論で「うーん、この研究分野やテーマ自体は悪くないと思うんだけど、研究してみた結果分かった事が今後役立つかって言われたら微妙だよねー。ていうか歴史研究に意義とか求めるなよなぁ」なんて思ってる大学生が前文に書くような言い訳を述べた所で、コードを書いていきましょうか。

pos-id

形態素の情報は、上で書いたようにmecab_node_t構造体のfeatureからアクセスできます。が、これは文字列(CSV形式に則っているそうです)なので、ちょっと扱い難い。よって、pos-id というものを使いましょう。pos-id というのは、上記の形態素の情報を分類して番号付けしたものがあって(名詞かつ一般名詞なら38、とか)、その自分に当てはまる番号を格納したものです。
どういう基準で番号付けが行われるかは、辞書データの中の pos-id.def ファイルに書いてあります。またこれを書き換えれば、オリジナルの基準で番号付けをする事もできます。詳しくはMeCabの公式サイトを見て下さい。
今回は最初から入っている pos-id.def の分類番号付けをそのまま利用しましょう。決して、再度辞書をコンパイルするのが面倒だったわけではありません。

コードは以下のとおりになりました。

コード

anly.d
import mecab;
import std.stdio;

void analysis(mecab_t* mecab, string filename) {
    File file;
    try {
        file = File(filename, "r");
    } catch {
        return;
    }
    uint[string] info = ["名詞":0, "動詞":0, "形容詞":0, "副詞":0, "接続詞":0];
    uint count;
    const(mecab_node_t)* node;

    foreach(line; file.byLine) {
        node = mecab_sparse_tonode2(mecab, line.ptr, line.length);
        for(; node; node = node.next) {
            if (node.stat != MECAB_NOR_NODE && node.stat != MECAB_UNK_NODE) continue;
            ++count;
            switch(node.posid) {
                case 10: .. case 12:
                    ++info["形容詞"];
                    break;
                case 26:
                    ++info["接続詞"];
                    break;
                case 31: .. case 33:
                    ++info["動詞"];
                    break;
                case 34: case 35:
                    ++info["副詞"];
                    break;
                case 36: .. case 47:
                    ++info["名詞"];
                    break;
                default:
                    break;
            }
        }
    }
    printResult(filename, info, count);
}

void printResult(string filename, uint[string] info, uint i) {
    import std.algorithm, std.conv;

    writeln("filename: ", filename);
    writeln(" * * *");

    writefln("単語数:\t%s", i);
    auto words = info.keys.sort!((a, b) => info[a] > info[b]);
    auto count = i.to!double;
    foreach(ref k; words)
        writefln("%s:\t%s\t%.3f%%", k, info[k], (info[k]/count)*100);

    writeln("");
}

void main(string[] args) {
    if(args.length == 1) return;

    auto mecab = mecab_new2("");
    scope(exit) mecab_destroy(mecab);

    foreach(text; args[1..$])
        analysis(mecab, text);
}

解説

switch文で範囲指定できるのはDの良いところだと思います。
pos-id.def を見ればわかるのですが、48番から67番までも名詞に分類されています。が、「接尾語や代名詞はちょっと違うくないか?」と思ったので、47番までを「名詞」としています。何か学術的な根拠があってこれらを除いたわけではないです。
mecab_sparse_tonode()ではなくmecab_sparse_tonode2()なる関数を使っていますが、これは文字列へのポインタと長さを一緒に受け取る関数です。byLine()で分割したらnullターミネーター付かないんじゃないの? と思ったのでこちらを使いましたが、byLine()の定義をしっかり見たわけではないので確証はありません。書いていませんが、勿論mecab.diの方にも定義を追加しています。

使ってみる

さて、分析してみましょう。
今回分析対象とする作家は以下の4人で、右が対象の小説です。

  • 夏目漱石 : 夢十夜
  • 泉鏡花 : 夜行巡査
  • 森鴎外 : 寒山拾得
  • 芥川龍之介 : 羅生門

全て青空文庫から入手できます。
今回利用させて頂いたテキストも青空文庫からダウンロードしたものですが、青空文庫のテキストだと、表示できない文字を※で代用しています(その後ろに文字の説明が続きます)。今回の分析では、それらの文字は単純に文章から取り除きました(形態素解析に影響が出そうですが、考えない事にします)。

入出力

$ dmd anly.d -L-lmecab
$ ./anly "akutagawa.txt" "ogai.txt" "soseki.txt" "kyoka.txt"
filename: akutagawa.txt
 * * *
単語数:  3884
名詞: 648 16.684%
動詞: 533 13.723%
副詞: 87  2.240%
形容詞:  65  1.674%
接続詞:  54  1.390%

filename: ogai.txt
 * * *
単語数:  4123
名詞: 736 17.851%
動詞: 653 15.838%
副詞: 81  1.965%
形容詞:  40  0.970%
接続詞:  28  0.679%

filename: soseki.txt
 * * *
単語数:  10796
名詞: 1967    18.220%
動詞: 1718    15.913%
副詞: 297 2.751%
形容詞:  255 2.362%
接続詞:  132 1.223%

filename: kyoka.txt
 * * *
単語数:  6815
名詞: 1144    16.787%
動詞: 956 14.028%
副詞: 278 4.079%
形容詞:  131 1.922%
接続詞:  41  0.602%

さぁ上手く出力できました、が……。
この結果は果たして合っているのか。確かめる手段が無いですね。
検算のプログラムを書いてみようにも、私のCやC++の腕前ではそっちでミスる可能性の方が高いです。
まぁそれっぽい感じの結果なので合っているという前提で進めましょうか。

結果を見る

value でソートしたのですが、全部順序は同じですね。これらの品詞の頻度は大体どの文章でも似通ったものになる、という事なのか。
細かい差異は色々とありますが……何かその、「これが傾向だ」みたく言える程の違いが見て取れないのが辛いです。
もっとサンプル数増やして何らかの統計的な計算でもすれば、何かしら有意な結果を導き出せるかもしれません。
なんというか、ぐだぐだな結果ですね。
ごめんなさい

その他の応用

今回の例はとても貧弱なものですが、それは全て私個人のプログラミングの能力に依るもので、MeCabや、D言語の性能とは関係ありません。
むしろMeCab、D言語の性能をちゃんと引き出せば、もっと複雑な事をもっと容易く、そして高速に行えるでしょう。

終わりに

さてD言語からMeCabを使う、どうだったでしょうか。
恐らくD言語からCのライブラリを使うなんていうのは、D言語erの間では空気のように当たり前の事のようなので、敢えてこういう事を書く人はあんまりいなかったのではないかと思います(私にとっては大変な事でした)。
でもC++を使うならどうだろう? と思ってこの投稿を書こうと思ったのですが、そもそもC++のAPIをDから使うのに失敗するという……。
しかも志を屈したにも関わらず、完全には使いこなせなかったのです。
以下、それに解する解説です。

課題

MeCabのサンプルを見た方であれば、mecab_dictionary_info_tを調べる部分が少し違うな、とお気付きになったかもしれません。

libmecab.html
const mecab_dictionary_info_t *d = mecab_dictionary_info(mecab);
for (; d; d = d->next) {
    printf("filename: %s\n", d->filename);
    printf("charset: %s\n", d->charset);
    printf("size: %d\n", d->size);
    printf("type: %d\n", d->type);
    printf("lsize: %d\n", d->lsize);
    printf("rsize: %d\n", d->rsize);
    printf("version: %d\n", d->version);
}

サンプルに載ってるコードでは、forループを使ってリストになっているmecab_dictionary_info_t構造体を全て辿っていますね。今回私が使用した環境では、辞書は1つしかないので、nextの先にはnullが入っている……と私は思ったのです。実際、CのコードやC++のコードはそういう構造を想定しているように見えます。

しかし、Dからmecab_dictionary_info()関数を使ってmecab_dictionary_info_t構造体を作ると、何故かnextがnullではありませんでした。nullではないのですが、何が入っているのかがわからない……。

import mecab;
import std.stdio;
import std.conv : to;

void main()
{
    auto mecab = mecab_new2("");
    scope(exit) mecab_destroy(mecab);

    auto dic = mecab_dictionary_info(mecab);

    if(dic) writeln("dicはある。");
    writeln(dic.filename.to!string);

    dic = dic.next;
    if(dic) writeln("dicはまだある。");
    writeln(dic.filename.to!string); // ここでSegmentation fault
}
$ ./mtf3
dicはある。
/var/lib/mecab/dic/debian/sys.dic
dicはまだある。
Segmentation fault

さて、dic.nextには何が入っているのか。
const(mecab_dictionary_info_t)*型なのは確実なのですが……。
結局わからず仕舞いでした。
力不足でごめんなさい。

以上です。

(12月24日追記)
教えて頂きました!
以下に、その修正と解説を追記します。

問題解決

コメントで色々と教えて頂きました。
そこで、記事の訂正を以下に記します。

DからC++のクラスを呼ぶ

Taggerのメンバ関数にbool parse(Lattice)が定義されてなかったのがまずかったようです。
なので、この関数を定義してあげます。ただし引数がLatticeクラスなので、先にこのクラスを定義しなければなりません。
よって、

extern(C++, MeCab)
{
    interface Lattice
    {
    }

    interface Tagger
    {
        bool parse(Lattice);
        const(char)* parse(const(char)*);
    }

    Tagger createTagger(const(char)*);
}

とすれば良いようです。
……ようです、と曖昧な書き方をしているのは、私が未だに、プログラム中でそれぞれの呼び出しがどこの定義を参照しているか理解していないからです。

ともかく、DからC++を呼び出す事はちゃんとできるようです。
素晴らしいです。
他にMeCab::Node等を定義していけば、今回実行した例をC++の呼び出しで実行する事もできそうです。

課題の解決

最後の課題ですが……これは完璧に愚かな私に原因がありました。

MeCabのmecab_dictionary_info_tは以下のように定義されています。

struct mecab_dictionary_info_t {
  const char                     *filename;
  const char                     *charset;
  unsigned int                    size;
  int                             type;
  unsigned int                    lsize;
  unsigned int                    rsize;
  unsigned short                  version;
  struct mecab_dictionary_info_t *next;
};

これを私は、次のようにDに翻訳しました。

struct mecab_dictionary_info_t {
    const(char)* filename;
    const(char)* charset;
    uint size;
    int type;
    uint lsize;
    uint rsize;
    mecab_dictionary_info_t *next;
}

おやぁ? 何か足りませんな。
はい、そうです。versionフィールドを省いていました。
「あれ、versionってDのキーワードじゃん。困ったな……まぁ定義無しでも参照しなけりゃいいか」
いいわきゃありませんでした
CでもDでも、フィールドを上から順番に並べていくのが構造体の作法ですものね。
ちゃんとキーワードと同名の変数を定義する方法がありました。公式サイトにばっちり書いてありました
htod使っておけばこんな事には……。
弁解の余地の無いレベルの大ポカです。本当に申し訳ありませんでした。

というわけで、DにもMeCabにも一切の瑕疵が無かった事が証明されました。

おまけ dicの正体

もうここまで読んでいただければ明らかな事ですが、

void main()
{
    auto mecab = mecab_new2("");
    scope(exit) mecab_destroy(mecab);

    auto dic = mecab_dictionary_info(mecab);

    writeln(dic.filename.to!string);
    dic = dic.next;
    writeln(cast(ushort)dic);
}
$ dmd tmf4.d -L-lmecab
$ ./tmf4
/var/lib/mecab/dic/debian/sys.dic
102

dic.nextの中にいたのはversionさんでした

さて、dic.nextには何が入っているのか。
const(mecab_dictionary_info_t)*型なのは確実なのですが……。

ここからして間違ってたわけですね。
これは恥ずかしい。

cedretaber
ペーパー司書。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away