LoginSignup
2
1

RustでGlobal ConstなHashMapを使おうと思ったらはまった

Last updated at Posted at 2023-07-20

はじめに

Global「変数」は使わなくても、Global「定数」なら許される派としては、constのHashMapを作りたいと思ったら意外と苦労した話。

constなHashMapに関すること

とりあえず。

最近はやりのChat GPTは課金しとらんから、Bing Chat先生に聞くとするか。

質問
Rustでconstなhashmapを作りたいのでやり方を教えてください。

回答
Rustのstd::collections::HashMapは、動的メモリ割り当てが必要なため、直接const HashMapを作成することはできません。 一つの方法として、lazy_staticクレートを使用して、実行時に静的変数を遅延初期化することができます1。

また、phfクレートを使用することもできます。これはコンパイル時に静的コレクションを作成することができます2。

例えば、次のように書くことができます:

use phf::{phf_map};

static COUNTRIES: phf::Map<&'static str, &'static str> = phf_map! {
    "US" => "United States",
    "UK" => "United Kingdom",
};

lazy_static→OnceCell→OnceLock

lazy_static

とりま、第1選択はlazy_staticなんかな、とりあえずググったろ。

実際、1.70.0から標準ライブラリにOnceCellが実装されとるな。
さっさとバージョンを上げてOnceCellを使ったろ。

rustup update

OnceCell

とりま、OnceCellを使った版を作ってみるか(ほとんどBing Chat先生作)。

main.rs
use std::cell::OnceCell;
use std::collections::HashMap;

static MY_MAP: OnceCell<HashMap<&'static str, &'static str>> = OnceCell::new();

fn init_my_map() {
    let map = MY_MAP.get_or_init(|| {
        let mut m = HashMap::new();
        m.insert("one", "uno");
        m.insert("two", "dos");
        m.insert("three", "tres");
        m
    });
    println!("my_map : {:?}", map);
    println!("my_map : {:?}", MY_MAP);
}
fn main() {
    init_my_map();
    println!("{:?}", MY_MAP);
}

なんかコンパイラに怒られるな。スレッドセーフじゃないからOnceLock使えってこと?別にマルチスレッドに対応する気もないからどうでもええんやけど。しゃーないから、OnceLockに変更するか。

error[E0277]: `OnceCell<HashMap<&'static str, &'static str>>` cannot be shared between threads safely
 --> src\main.rs:4:16
  |
4 | static MY_MAP: OnceCell<HashMap<&'static str, &'static str>> = OnceCell::new();
  |                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `OnceCell<HashMap<&'static str, &'static str>>` cannot be shared between threads safely
  |
  = help: the trait `Sync` is not implemented for `OnceCell<HashMap<&'static str, &'static str>>`
  = note: if you want to do aliasing and mutation between multiple threads, use `std::sync::OnceLock` instead
  = note: shared static variables must have a type that implements `Sync`

OnceLock

完成!

use std::collections::HashMap;
use std::sync::OnceLock;

static MY_MAP: OnceLock<HashMap<&'static str, &'static str>> = OnceLock::new();

fn init_my_map() {
    let map = MY_MAP.get_or_init(|| {
        let mut m = HashMap::new();
        m.insert("one", "uno");
        m.insert("two", "dos");
        m.insert("three", "tres");
        m
    });
    println!("my_map : {:?}", map);
    println!("my_map : {:?}", MY_MAP);
}
fn main() {
    init_my_map();
    println!("{:?}", MY_MAP);
}

ファイル分割に関すること

#path[="path/to/source.rs"]の勘違い

Global定数なんて、ファイル分割しないとあまり意味ないしな。とりま、「rust ファイル分割」でググるか。なんや、2つまでの例はあるけど、3つ以上の奴は少ないな。お、良さそうな奴があるやんけ。
ほーん、#[path=]を使ったら隣のファイルもmodできるんやな(大いなる勘違い)。

main.rs
mod my_map;
use my_map::{init_my_map, MY_MAP};

mod another_file;
use another_file::print_my_map;

fn main() {
    init_my_map();
    println!("main : {:?}", MY_MAP);

    print_my_map();
}
my_map.rs
use std::collections::HashMap;
use std::sync::OnceLock;

pub static MY_MAP: OnceLock<HashMap<&'static str, &'static str>> = OnceLock::new();

pub fn init_my_map() {
    let map = MY_MAP.get_or_init(|| {
        let mut m = HashMap::new();
        m.insert("one", "uno");
        m.insert("two", "dos");
        m.insert("three", "tres");
        m
    });
    println!("my_map : {:?}", map);
    println!("my_map : {:?}", MY_MAP);
}
another_file.rs
#[path = "./my_map.rs"]
mod my_map;
use my_map::MY_MAP;

pub fn print_my_map() {
    println!("another_file : {:?}", MY_MAP); // <- Once(Uninit)
}

なんか変なwarningが出てるけど、コンパイル通るし行けるやろ。
init_my_mapが使われてないといわれとるけど、ちゃんと使っとるし、初期化されとるやろ。
ん?最後だけ未初期化に戻っとるぞ???

cargo run
   Compiling global_static_hashmap v0.1.0 (C:\Users\tagaw\rust\global_static_hashmap)
warning: function `init_my_map` is never used
 --> src\my_map.rs:6:8
  |
6 | pub fn init_my_map() {
  |        ^^^^^^^^^^^
  |
  = note: `#[warn(dead_code)]` on by default

warning: `global_static_hashmap` (bin "global_static_hashmap") generated 1 warning
    Finished dev [unoptimized + debuginfo] target(s) in 0.51s
     Running `target\debug\global_static_hashmap.exe`
my_map : {"one": "uno", "three": "tres", "two": "dos"}
my_map : Once({"one": "uno", "three": "tres", "two": "dos"})
main : Once({"one": "uno", "three": "tres", "two": "dos"})
another_file : Once(Uninit)

「use crate::」の意味を知る

とりま、Bing Chat先生に聞くか。

質問
rustの以下のプログラムにおいて、改善点を示してください。
// main.rs
// 略
// my_map.rs
// 略
// another_file.rs
// 略

回答
このRustプログラムにはいくつかの改善点があります。

  1. my_map.rs と another_file.rs の両方で MY_MAP を使用していますが、another_file.rs では #[path = "./my_map.rs"] を使用して my_map モジュールをインポートしています。これは、main.rs で my_map モジュールを公開しているため、不要です。代わりに、another_file.rs で use crate::my_map::MY_MAP; を使用することができます。
    // 略

ほーん、use crate::って書くとmainからの相対パスでかけるんやな。

another_file.rs
-#[path = "./my_map.rs"]
-mod my_map;
-use my_map::MY_MAP;
+use crate::my_map::MY_MAP;

 pub fn print_my_map() {
     println!("another_file : {:?}", MY_MAP);

完成!ぱーぺき!!!

#[path=]について

その後、Bing Chat先生に#[path=]について少し質問。

#[path = "..."]属性を使用すると、モジュールが定義されているファイルのパスを明示的に指定することができます。これは、モジュールが定義されているファイルが、通常の場所(つまり、モジュール名と同じ名前のファイル)にない場合に便利です。
ただし、#[path = "..."]属性を使用すると、コードの可読性が低下する可能性があります。モジュールが定義されているファイルの場所が明示的に指定されているため、コードを読む人がその場所を追跡する必要があります。また、ファイルの場所が変更された場合、#[path = "..."]属性も更新する必要があります。
一方、modキーワードを使用すると、モジュールが定義されているファイルの場所を自動的に検索します。これにより、コードの可読性が向上し、ファイルの場所が変更された場合でも、コードの変更は不要です。
したがって、特別な理由がない限り、modキーワードを使用してモジュールを定義することが推奨されます。

つまり、サブモジュールとして扱うことと同義なんだと理解し、以下のように書けば一応動くことを確認。(GlobalなMY_MAPがanother_fileのサブモジュールに配置されるという上下関係が逆になることが気持ち悪いが・・。)

main.rs
mod another_file;

use another_file::my_map::{init_my_map, MY_MAP};
use another_file::print_my_map;

fn main() {
    init_my_map();
    println!("main : {:?}", MY_MAP);

    print_my_map();
}
another_file.rs
#[path = "./my_map.rs"]
pub mod my_map;

use my_map::MY_MAP;

pub fn print_my_map() {
    println!("another_file : {:?}", MY_MAP);
}
my_map.rs
use std::collections::HashMap;
use std::sync::OnceLock;

pub static MY_MAP: OnceLock<HashMap<&'static str, &'static str>> = OnceLock::new();

pub fn init_my_map() {
    let map = MY_MAP.get_or_init(|| {
        let mut m = HashMap::new();
        m.insert("one", "uno");
        m.insert("two", "dos");
        m.insert("three", "tres");
        m
    });
    println!("my_map : {:?}", map);
    println!("my_map : {:?}", MY_MAP);
}

所感

ファイル分割が不適切な版が最後だけ未初期化に戻るのかは理解できないし、never usedと警告が出るのも謎。

免責事項(言い訳)

PythonやRubyで書いたコードをBing Chat先生に投げてRustに変換してもらった後にふむふむ言っている初心者なので、あてにしないように!!!

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1