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

こんにちは、Rust初心者のsonesuke( https://twitter.com/sonesuke )です。

[この記事](https://qiita.com/sonesuke/items/0f9cd1baf8d11f3f4e16)で、PostgreSQLの拡張を作っていたのですが、どうしてもやりたい機能がありました。

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

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

# TL;DR

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

https://github.com/sonesuke/postgresql-ja-tokenizer/blob/main/ja_tokenizer/src/lib.rs

# 要件定義 (おさらい)

[前回の記事](https://qiita.com/sonesuke/items/0f9cd1baf8d11f3f4e16)で、形態素解析するPostgreSQLの拡張 (ユーザー定義関数) を作ったのですが、標準辞書だけでなく、ユーザー定義辞書も使いたかったのです。(使わないと何がおきるかは[こちら](#実験)

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

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

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

なんて贅沢な。。。。

# 試行錯誤

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

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

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

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

。。。。。だめでした。

```bash
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](https://tomoyuki-nakabayashi.github.io/book/peripherals/singletons.html)

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

https://stackoverflow.com/questions/27791532/how-do-i-create-a-global-mutable-singleton

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

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

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

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

ググります。。。。。

https://zenn.dev/frozenlib/articles/lazy_static_to_once_cell

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

```rust
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<...>>` で宣言している

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

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

```rust
let mut t = TOKENIZER
        .get_or_init(|| Mutex::new(Tokenizer::new().unwrap()))
        .lock()
        .unwrap();
    *t = Tokenizer::with_config(config).unwrap();
```
ですね。 

# 実験

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

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

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

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

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

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

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

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

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

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

# 考察とまとめ

`once_cell`と`Mutex`を使うことで、

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

ことができました。

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

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