Rustで可変 Singletonしたいんだけど、どうしたらいい?


こんにちは、Rust初心者のsonesukeです。

この記事で、PostgreSQLの拡張を作っていたのですが、どうしてもやりたい機能がありました。

ユーザー定義辞書が使いたい。

可変のSingletonという発想からスタートしたのですが、めちゃくちゃ試行錯誤したという話です。

TL;DR

  • Rustでミュータブルなシングルトンをやりたかったら、 once_cellMutex を組み合わせる。
  • コードはこちら

要件定義 (おさらい)

前回の記事で、形態素解析するPostgreSQLの拡張 (ユーザー定義関数) を作ったのですが、標準辞書だけでなく、ユーザー定義辞書も使いたかったのです。(使わないと何がおきるかはこちら

まじめに要件定義すると、下記になります。

  1. ユーザ定義辞書が変わったら、初期化をやりなおしたい
  2. 初期化は可能な限り少なくしたい → 都度初期化するのはパフォーマンス的になし

つまり、基本的にlinderaの初期化は事前に済ませておきたいのだけど、ユーザー定義辞書を使うときは、 初期化をやり直したいと・・・・

なんて贅沢な。。。。

試行錯誤

初期化したものを、ずっと持っていたい・・・・ということは、C言語脳だと、「 static なグローバル変数を持てばいいじゃん」的なことを考えてみました。

use lindera::tokenizer::Tokenizer;
use lindera_core::LinderaResult;

static mut tokenizer = Tokenizer::new().unwrap();

fn main() -> LinderaResult<()> {
    let tokens = tokenizer.tokenize("新しい日本の夜明け")?;
...

。。。。。だめでした。

error: missing type for `static mut` item
 --> src/main.rs:4:12
  |
4 | static mut tokenizer = Tokenizer::new().unwrap();
  |            ^^^^^^^^^ help: provide a type for the static variable: `tokenizer: Tokenizer`

static mut では、 static variable が必要。。。。
このあと、少し試行錯誤するもうまくいかず。。。。諦めかけた矢先、目に飛び込んだのが・・・

しかし、これにはいくつか問題があります。 これはミュータブルなグローバル変数であり、Rustにおいては、これらとやり取りするのは常にアンセーフです。 これらの変数はプログラムの全体を通して見えることになり、つまりそれは借用チェッカがこれらの変数の参照や所有権を追跡するのに役立たなくなることを意味します。
The Embedded Rust Book

救いがない・・・・この先、頑張ったとしても unsafe の烙印を押されるんだ。
でも、そういうことやりたい人は、どうすれば? この悩みを持っている人は、人類初ではないはず。。。。

いた! 自分以外にも同じ悩みを持っている人が!

lazy-static を使う方法と、 once_cell をつかう方法があるらしい。方針としては、 staticなものとして、 Mutex をもち、その中身をスレッドセーフに書き換えると・・・・

恐るべし、コンパイラの警告からここまでを思いつくのだろうか・・・・

それはさておき、方針がはっきりしたところで、lazy-static か、 once_cell か、どちらに?

ググります。。。。。

once_cell が優勢ですね。標準に組み込まれる勢いです。
その方針に従って、改めて書き直すと下記。

static TOKENIZER: OnceCell<Mutex<Tokenizer>> = OnceCell::new();

#[pg_extern]
fn jat_tokenize(input: &str) -> impl std::iter::Iterator<Item = String> {
    let t = TOKENIZER.get_or_init(|| Mutex::new(Tokenizer::new().unwrap()));
...
}

#[pg_extern]
fn jat_config(input: &str) -> &str {
...
    let mut t = TOKENIZER
        .get_or_init(|| Mutex::new(Tokenizer::new().unwrap()))
        .lock()
        .unwrap();
    *t = Tokenizer::with_config(config).unwrap();
...
}

ポイントは 最初の OnceCell<Mutex<...>> で宣言している

static TOKENIZER: OnceCell<Mutex<Tokenizer>> = OnceCell::new();

と、実際に lock してから *t を書き換えをする

let mut t = TOKENIZER
        .get_or_init(|| Mutex::new(Tokenizer::new().unwrap()))
        .lock()
        .unwrap();
    *t = Tokenizer::with_config(config).unwrap();

ですね。

実験

まずは、ユーザ定義辞書を使わない状態で形態素解析を走らせます。

ja_tokenizer=# CREATE EXTENSION ja_tokenizer;
CREATE EXTENSION
ja_tokenizer=# SELECT jat_tokenize('東京スカイツリーの最寄り駅はとうきょうスカイツリー駅です');
 jat_tokenize
--------------
 東京
 スカイ
 ツリー
 
 最寄り駅
 
 とう
 きょう
 スカイ
 ツリー
 
 です
(12 rows)

見事に、東京スカイツリーが、ばらばらの、東京スカイツリーに分解されていますね。固有名詞としての東京スカイツリーが、単語として標準辞書に登録されていないためです。

では、ユーザー定義辞書として、下記を登録してみます。 linderaのサンプルそのまんまです。

user_dic.csv
東京スカイツリー,1288,1288,-1000,名詞,固有名詞,一般,カスタム名詞,*,*,東京スカイツリー,トウキョウスカイツリー,トウキョウスカイツリー
東武スカイツリーライン,1288,1288,-1000,名詞,固有名詞,一般,カスタム名詞,*,*,東武スカイツリーライン,トウブスカイツリーライン,トウブスカイツリーライン
とうきょうスカイツリー駅,1288,1288,-1000,名詞,固有名詞,一般,カスタム名詞,*,*,とうきょうスカイツリー駅,トウキョウスカイツリーエキ,トウキョウスカイツリーエキ

改めて、ユーザー定義辞書をつかうようにしてみます。

ja_tokenizer=# SELECT jat_config('/app/sample/user_dic.csv');
 jat_config
------------
 /app/sample/user_dic.csv
(1 row)

無事に読み込んでくれた模様。それでは、形態素解析を走らせてみましょう。

ja_tokenizer=# SELECT jat_tokenize('東京スカイツリーの最寄り駅はとうきょうスカイツリー駅です');
       jat_tokenize
--------------------------
 東京スカイツリー
 
 最寄り駅
 
 とうきょうスカイツリー駅
 です
(6 rows)

東京スカイツリーがひとつの単語として、認識されていますね。実験成功です。

考察とまとめ

once_cellMutexを使うことで、

  1. アクセス時に必ず、初期化済みのものにアクセスできる
  2. 書き換えはスレッドセーフに行われるという状態を作り出す

ことができました。

他の言語だと、気づかず、初期化漏れ、スレッド競合のバグを作り込んでしまいがちです。
Rustだとコンパイルエラーから、自然(無理矢理)に安全なコードに導いてくれます。Rustの哲学を感じますね。

初心者が少し調べて思い至った結論なので間違っている可能性があります。もっとよい方法があれば、ご教示いただけると幸いです。