Rustで設定値を持つにはどうしたらいいですか

κeenです。Rustではじめてアプリケーションを書くときに困りがちなことの1つにグローバルな値を持つにはどうしたらいいか分からないというのがあるようです。 その書き方を何パターンか紹介しできたらなと。

一応この記事には元ネタというかインスパイア元があり、以下のリポジトリも見ながら書かれています。

https://github.com/paulkernfeld/global-data-in-rust

let にする

一番よくあるケースだとCLIや起動時に読み込んだ設定ファイルの値をどうするかでしょう。データの性格として起動時に一度だけ生成してあとは(ほとんど)いじらないようなものです。そういうものは main 関数内で let して、あとは借用で持ち回ればよいです。

起動時にファイルかろ設定値を読み込んで動くアプリケーションを雰囲気で書くとこんな感じです。

use std::io;

struct Config {
    limit: u32,
}

fn run(config: &Config) -> io::Result<()> {
    // do something
    Ok(())
}


fn read_config_from_file() -> io::Result<Config> { /* */ }

fn main() -> io::Result<()> {
    // 読み込んだ値を `main` 内の `let` で保持する
    let config = read_config_from_file()?;
    // 借用して関数の引数に渡す
    run(&config)
}

アプリケーションが巨大になるとやや引数で持ち回るのが面倒ですが、一番素直な選択肢です。

メソッドにする

let の亜種も紹介しておきます。データの持ち方としては let ですが、それを関数の引数で持ち回るのではなく、メソッドを生やしてメソッドでやりとりするものです。データの性格として、起動時に一度だけ生成するのは let と共通ですがただの値ではなくメソッドなどが生えた外部ライブラリのデータ型などが多いでしょう。

起動時にコネクションプールを作って動くアプリケーションを雰囲気で書くとこんな感じです

use some_crate::ConnectionPool;
use std::io;

struct App {
    pool: ConnectionPool,
}

impl App {
    fn run(&mut self) -> io::Result<()> {
        let cn = self.pool.get_conn()?;
        // do something
        Ok(())
    }
}

fn main() -> io::Result<()> {
    let pool = ConnectionPool::new()?;
    // 生成した値を `main` 内の `let` で保持する
    let mut app = App { pool };
    // メソッドを呼び出す
    app.run()
}

コンフィグでもこれをやってもいいのですが、分けた方が失敗が少ないかなと思っています。典型的な App&mut self を要求するのに対して大抵の場合 Config は可変性が不要で、本来なら問題ないはずのコードも &mut self の借用チェックに引掛ってコンパイルが通らないケースがたまに発生するからです。そういうケースが発生しないのであれば App のフィールドに config: Config を生やすとよいです。

const にする

環境変数から値を受け取るときの環境変数名など、アプリケーション側で決める値を保持するときは const が候補にあがります。 const にはヒープを扱う式が書けず、値もコピーされるのでデータの性格としてリテラルで書ける程度の軽いものなどです。

アプリケーションが読み込む環境変数名を const に持つコードを雰囲気で書くとこんな感じです。

use std::io;
use std::env;

const APP_HOME_ENV: &str = "APP_HOME";

fn main() -> io::Result<()> {
    let home = env::var(APP_HOME_ENV).unwrap_or_else(|_|"/var/app".into());
    // do something
    Ok(())
}

lazy_static / once_cell にする

アプリケーション側で決める値を初期化したいがヒープを使うので conststatic で使えないような場合です。 このような場合は外部クレートの lazy_staticonce_cell を使うことになるでしょう。 データの性格としてコンストラクタを介さないといけないようなものが多いです。

regex のパターンを初期化するコードはこう書けます。

// regexのドキュメントより

use lazy_static::lazy_static;
use regex::Regex;

fn some_helper_function(text: &str) -> bool {
    lazy_static! {
        static ref RE: Regex = Regex::new("...").unwrap();
    }
    RE.is_match(text)
}

fn main() {}

おおむねシングルトンパターンのようなものです。


大抵のケースではこの記事に書いた方法のどれかで事足りるでしょう。 冒頭に貼った元ネタのリポジトリではもう少しバリエーションが紹介されているのでやりたいことが解決できなかったらそちらも覗いてみて下さい。

Written by κeen