【Rust RFC 3058: Try Trait v2】?演算子(Question Operator)の新しい挙動についてのまとめ

この記事では現在RustのRFC3058として提案されている,Try Trait v2について解説していきます.

とはいっても私はまだRustを学習している段階で,RFCの背景や実際になされている議論などには踏み込んだ説明をすることはできませんのでそこはご了承ください. もっと詳しく知りたいという方はRFCのページを見ると全て書いてあります.

rust-lang.github.io

このRFCは今も議論が続けられているのでもしかしたら大幅に変わる可能性もあります.ご了承ください.

また,v2という名前から察する人もいるかもしれませんが,同じ目的のv1もありました(今はv2に置き換えられています). rust-lang.github.io そちらについては書かないのか,と思う方もいるかもしれませんが,機能の説明にあたってはv1との差分をそこまで気にする必要はなさそうだと判断したため省いています. 実際のAPIのデザインに当たってはv1との差分を考えているようでしたので興味がわいた方は調べてみると面白いと思います(そしてできれば私に教えてください).

?演算子の現在の挙動

このRFC(以下try_trait_v2)は,?演算子の挙動を新しくするという目的で提案されています.

そこでまずは?演算子の現在の挙動についておさらいしていきます.

The Rust Programming LanguageReferenceに書かれているように,?演算子は「評価するとResult<T,E>Option<T>になるような式の後ろにつけることで,OkSomeならTと評価され,ErrNoneならその関数からreturnする」というような挙動をします.

つまり

// この式は
expression?

// こう解釈されます
// Optionの場合にはSome(v)がvに,Noneはreturn Noneになります
match expression {
    Ok(v) => v,
    Err(e) => return Err(e)
}

ということです(本当は嘘ですがそれは後述します).

このように書くと少しわかりにくいので実際のコードで説明します.

fn ret_ok() -> Result<isize,()> {
    Ok(10)
}

fn ret_err() -> Result<isize,()> {
    Err(())
}
fn print_if_ok() -> Result<(),()> {
    println!("{}",ret_ok()?);
    // println!("{}",ret_err()?);

    println!("print succeed");
    Ok(())
}

fn main() {
    match print_if_ok() {
        Ok(_) => println!("ok"),
        Err(_) => println!("err")
    }
}

このコードを実行すると以下のように出力されます.

10
print succeed
ok

一方,print_if_ok関数内のコメントを外すと

10
err

というように表示されます.

まず,ret_ok関数はOk(10)を返します. そのためret_ok()?という式は全体として10と評価され,結果として10と表示されます.

一方コメントを外すと10と表示されるところまでは同じですが,ret_err関数はErr(())を返すため,ret_err()?という式はreturn式として評価されます.*1

この例だとあまり使い道がわからないような気がしますが,よく使われるケースとしてはエラーの伝搬が挙げられます.

Resultを返す関数の中でResultを返す関数を呼ぶとき(例えばファイルを読んでその内容を出力するみたいな関数とかの中にはエラーを起こしうる関数呼び出しが含まれます),成功したらOkの中身を使いたいんだけど失敗したらそのままエラーをreturnしてほしい,という処理はよくあると思いますが,そのようなときに?演算子を使うことでより簡潔に書くことができます.

さて,例を挙げて現在の?演算子の挙動について説明しましたが,まだ一つ説明していない点があります. 先ほど式がmatch式に解釈され,その中でErr(e)return Err(e)となるようなmatchアームを書きましたが,これは厳密には正しくありません.

これを考慮すると,実際には下のようになります.

// この式は
expresstion?

// こう解釈される
// Optionの場合には先ほどと同じ
match expression {
    Ok(v) => v,
    Err(e) => return Err(From::from(e))
}

ほとんど変わってはいませんが,return Err(From::from(e))という部分だけが変わっています.

詳しくはFromトレイトのドキュメントを見ると分かりますが,これは「eの型(expressionがResult<T,E>ならE)から何かの型Uへの変換」を行っています.

これは,関数内で起こる様々なエラー型をまとめて一つのエラー型で表したうえでエラーを伝搬したい,といったケースで有効です. 例えばio::Errorstr::Utf8Errorなどを返すエラーを返しうる関数を呼ぶ関数において,独自エラー型MyErrorを返すようにしてそのエラー型に対しFrom<io::Error>From<str::Utf8Error>を実装することで,?演算子によって自動的にMyErrorに変換してくれます.

struct MyError;

impl From<std::io::Error> for MyError {
    fn from(e: std::io::Error) -> Self {
        MyError
    }
}

fn err() -> Result<(),MyError> {
    // ここではio::ErrorだけだがFromを様々なエラーに実装すると複数の型をまとめて扱える
    std::fs::File::open("notexist.txt")?;
    Ok(())
}

fn main() {
    err();
}

新しい?演算子に求められること

以上のように現在の?演算子は,「エラーがあったらreturnしてそうでないなら中の値が欲しい」という場合,非常に便利です.

しかし一方で,これを一般化した「何かの条件を満たすのであれば何かをreturnしてそうでないなら中の値が欲しい」というケースでは現状の?演算子では扱うことができません.

まず,?演算子ResultまたはOptionを返す関数内でしか使えません. プログラムを書く上では,プログラムを各対象に応じたよりリッチな型を返したい,と思うことは往々にしてあります. 例えば何かHTTPサーバーを作っているときに,Result型ではなくてHTTPResponseに対応した独自型を返したい,というケースなどです.

そこで新しい?演算子には,特定の型でのみ使えるのではなく任意の型に使えてほしいという要件が求められます.

また,従来でも実装されていた型変換の機能も当然求められます.

Try Trait v2

さて,新しい?演算子に求められている要件を整理したところで,ようやく本題であるRFC 3058: Try Trait v2について説明していきます.

Try Trait v2では,以下の3つのトレイトや型を用いて?演算子を実装します.

ControlFlow型

ControlFlow型は他の二つのトレイトとは異なり,すでにstableに取り入れられています.なぜControlFlow型だけ先にstabilizeされているのかはわからない*2んですが,多分Try Trait v2での議論とはある程度直行しているからとかだと思います.

この型はシンプルで以下のようなシグネチャになっています.

enum ControlFlow<B, C = ()> {
    Continue(C),
    Break(B),
}

この型によって,何か処理をしているときに,そのまま処理を続けるのか(Continue)それとも途中でやめるのか(Break)を表現することができ,さらにそれらは何か値を運ぶことができます*3

この型は,上の「エラーがあったとき」を「何かの条件を満たすとき」と一般化したことで必要になったであろう型です. というのもこの型の挙動自体はResult型でも特に問題なく実現できるのですが,その場合には非直感的になってしまうことが避けられないからです.

例としてグラフの探索など何か続けて処理をするときのことを考えます. 探索していた要素を見つけたので早期にリターンしたいと思った場合,Result型を使う場合だと,正常に見つかった場合でもErrバリアントを使う必要があり,これは非直感的です*4

つまり,Result型は「早期リターンするとき,それは何らかのエラーである」という前提を持っているといえます. この前提はResult型を返す関数に限って言えばある程度は正しそうですが,新しい?演算子では任意の型を返せる必要があるため明らかに間違いです.

そこでControlFlow型は,「早期リターンするのか,それとも続けるのか」という,正常・エラーの価値判断なしに使える型になっています.

この型を使うことで任意の型の値に対して早期リターンするのか処理を続けるのかという挙動を直感的に書けるようになりますが,実際にどのように書くのかは登場人物がすべて出そろってからまとめて紹介します.

Tryトレイト

Tryトレイトは名前からわかるように,Try Trait v2の中心となるトレイトで,このトレイトを実装した型に対して?演算子を使った短縮記法が使えるようになります.

このトレイトは以下のようなシグネチャになっています.

trait Try: FromResidual<Self::Residual> {
    type Output;
    type Residual;

    fn from_output(output: Self::Output) -> Self;
    fn branch(self) -> ControlFlow<Self::Residual, Self::Output>;
}

まだ解説していないFromResidualがスーパートレイトになっていたりしますが,重要なのは二つの関連型OutputResidualbranch関数です.

OutputResidual

この二つの関連型はそれぞれ「処理を継続するときの値」と「早期リターンするときの値」に対応します.

分かりやすいのはOutputの方で,let x = expression?としたとき,xの型はOutputとなる,といった具合なので特に説明はしません.

少しわかりにくいのがResidualの方で,こちらはそもそも単語レベルでなじみがありませんが*5,単語の意味としては「残余」というのがメジャーらしいです.

こちらはOutputとは逆に早期リターンするときの値の素になる値の型です.少しお茶を濁したような言い方をしている理由はFromResidualの説明で分かると思いますので,ここではリターンされる値なんだなという理解で十分です.

Residualという分かりにくい単語ではありますがイメージとしては,「正常」な処理による値(Output)の残りが「エラー」でリターンされる値(Residual)という感じでしょうか(イメージを分かりやすくするため,早期リターン=エラーという前提に立ちましたが前述のとおり正しくない使い方です).

まとめると,?演算子によって最終的に処理の中心となる値の型のうち,処理継続の時に使われるのがOutputで,早期リターンで使われるのがResidualという型ということです.

branch関数

?によってOutputResidualといった型の値に処理の対象が分岐していきますが,どのような時に処理を続けてどのような時に早期リターンするのか,というのを決定するのがbranch関数です.

この関数は下のようなシグネチャになっています.

fn branch(self) -> ControlFlow<Self::Residual, Self::Output>;

Tryトレイトを実装してある型Selfを引数にとり,ControlFlow型を返すことで処理を継続するのか,早期リターンするのかを決定します.

この動作は?演算子の本質といっても過言ではなく,新しい?演算子は下のコードのように解釈されます.

// このコードは
expression?

// このように解釈される
match Try::branch(expression) {
    ControlFlow::Continue(v) => v,
    ControlFlow::Break(r) => return r,  // 型変換が入るため実際にはこうではないがやっていることは似ている
}

FromResidualトレイト

ControlFlow型が処理の継続と早期リターンという一般化のための型で,Tryトレイトは?演算子の挙動の本質的な部分を担っているということを述べていきましたが,FromResidualトレイトは,?演算子のもう一つの役割である型変換を担っており,以下のようなシグネチャを持ちます.

trait FromResidual<R = <Self as Try>::Residual> {
    fn from_residual(residual: R) -> Self;
}

シグネチャからわかるように,何かの型R(何も指定しなければTryを実装する型のTry::Residual)から自身の型に変換するためのトレイトです.

上で?がどう解釈されるのかを書きましたが,このトレイトを使った型変換まで考慮に入れると下のような解釈になり,これは完全な解釈です.

// このコードは
expression?

// こう解釈される
match Try::branch(expression) {
    ControlFlow::Continue(v) => v,
    ControlFlow::Break(r) => return FromResidual::from_residual(r),
}

先ほどと違う部分はControlFlow::Breakバリアントだった時にreturnされる値がrからFromResidual::from_residual(r)になっている点です.

これによって,もしTry::Residualと返り値の型が異なっていても,返り値の型にFromResidualトレイトを実装することで適切に変換することが可能になります.

ここでやっていることはFromトレイトと同じですが,わざわざ別のトレイトを使うことで?のための変換とそれ以外の変換を分ける,という意図があるのだと思います*6

実際の使い方

さて,ここまでで新しい?演算子についてある程度は理解できたかと思いますが,じゃあ実際にはどう使うのかというのがまだわからない人もいると思います(私も説明読んだだけではわかりませんでした). そこで,簡単な例を使って使い方を解説していきます.

ここでは,C言語での返り値による実行ステータスを表す型を実装してみることにします. また,その型を返す関数の結果を使いResult型を返す関数を書くことで,型変換についても説明していきます.

#![feature(try_trait_v2)]
use std::num::NonZeroUsize;
use std::ops::{Try,FromResidual,ControlFlow};

// 0が成功,1以上なら失敗のステータスコード
struct Status(usize);

// StatusのResidualは成功でないものなのでこのように書ける
struct StatusResidual(NonZeroUsize);

impl FromResidual for Status {
    fn from_residual(r: StatusResidual) -> Self {
        // UsizeはFrom<NonZeroUsize>を実装しているので変換可能
        Self(r.0.into())
    }
}

impl Try for Status {
    type Output = ();
    type Residual = StatusResidual;
    
    fn from_output(_ : <Self as Try>::Output) -> Self {
        Self(0)
    }
    
    fn branch(self) -> ControlFlow<<Self as Try>::Residual, <Self as Try>::Output> {
        // NonZeroUsize::new(x)はxが0だったらNoneを,それ以外ならSome(x)を返す
        // つまりSomeなら失敗ステータスを表している
        match NonZeroUsize::new(self.0) {
            Some(r) => ControlFlow::Break(StatusResidual(r)),
            None => ControlFlow::Continue(()),
        }
    }
}

impl FromResidual<StatusResidual> for Result<(), NonZeroUsize> {
    fn from_residual(r:StatusResidual) -> Self {
        Err(r.0)
    }
}

// 実際には何かの処理を行ってそのステータスコードを返す関数
fn something_success() -> Status {
    Status(0)
}
fn something_error() -> Status {
    Status(100)
}

// 終了ステータスコードが返される関数を中で呼び出し,Resultに変換して返す
fn call_fn_and_return_result() -> Result<(), NonZeroUsize> {
    let x: () = something_success()?;
    let x: () = something_error()?;
    Ok(())
}

fn main() {
    match call_fn_and_return_result() {
        Ok(_) => println!("ok"),
        Err(sc) => println!("err with {}",sc),
    };
}

出力は

err with 100

となります.

そこまで説明のいらないコードだと思うので説明はしませんが,いくつか注意点があります.

まずは,このコードはnightlyなrustを使わないといけません. これは,このRFCが安定化されておらず,try_trait_v2featureゲートを使う必要があるためです.

次に,型変換を行っている

impl FromResidual<StatusResidual> for Result<(), NonZeroUsize> {
    fn from_residual(r:StatusResidual) -> Self {
        Err(r.0)
    }
}

についてですが,これは最終的に?演算子return FromResidual::from_residual(r)というように解釈されるため,StatusResidualであるStatusResidual型からcall_fn_and_return_result関数の返り値であるResult<(),NonZeroUsize>への変換が存在する必要があるためです.

最後に

とても長くなりましたが,自分なりにRFC 3058のまとめのようなものを書いてみました.

記事を書くのにあたって,rustのRFCやそのtracking issueを初めて(完全には追えてはいないものの)読んでみましたが,当初とはデザインが変わっているという旨がかかれていたり,型の名前があまり直感的ではないので直した方がよいという議論が上がっていたりとRustはこうやって作られているのだな,というのを少し体感できたような気がして,いつかRust本体にも関わってみたいなと思いました.

*1:returnは文ではなく,評価されると関数からreturnされる式なのを初めて知りました. Return expressions - The Rust Reference

*2:一応理由は書いてあるんですが少し意味が分からなかった

*3:ResultでのOkに対応していそうなContinueの運ぶ型がジェネリクスの二つ目に入っているのはなぜなんだろうか

*4:じゃあOkバリアントを使えばいいのでは?と思うかもしれませんが,それだと?による早期リターンが使えなくなります

*5:RFCの議論でもわかりにくいと言われている

*6:明確な根拠は見つからなかったので知っている人がいたら教えてくれると幸いです

dockerコンテナ内でホストからマウントされたファイルシステムにアクセスするときのパーミッションについて

今までdockerをなんとなくで触ってきていましたが,最近触ることがありその中で気づいたことがあるのでメモがてら書いておきます.

マウントされたファイルシステムにアクセスするときのパーミッションエラー問題

調べるといろいろ出てくると思いますが,コンテナにファイルシステムをマウント*1したとき,ホストマシンとコンテナ間でuidやgidがそのまま使われてしまうことによっていざアプリケーションを動かそうとしたりコンテナが書き込んだデータがroot所有になっていたりする,という問題がlinuxでは生じます*2

この問題に遭遇した時,様々な解決策が講じられていますが,とりあえずそれらは置いておいて,なぜこのような現象が起きるのかを理解するためにいくつか実験を行います.

実験その1 〜特に何も考えず書き込む〜

まずは特に何も考えずbind mountを行ってコンテナ内でマウントされたディレクトリに書き込みを行います.

以下のようなDockerfileを用意して

FROM debian:11-slim

CMD ["touch", "/hogehoge/test.txt"]
koutarou@koutarou-desktop/INS> docker run --rm -v $(pwd)/hogehoge:/hogehoge docker-permission-test

koutarou@koutarou-desktop/INS> ls
Dockerfile  hogehoge

koutarou@koutarou-desktop/INS> ls -l
合計 8
-rw-rw-r-- 1 koutarou koutarou   57  616 21:26 Dockerfile
drwxr-xr-x 2 root     root     4096  616 21:26 hogehoge

koutarou@koutarou-desktop/INS> ls hogehoge -l
合計 0
-rw-r--r-- 1 root root 0  616 21:26 test.txt

マウントしたhogehogeディレクトリにtest.txtというファイルを作るだけです. hogehogeディレクトリは走らせた時点で存在していないので勝手に(root:rootで)作られ,その中にはこれまたroot:rootなtest.txtができています.

それでは,予めhogehogeディレクトリがある状態で走らせるとどうでしょうか?

koutarou@koutarou-desktop/INS> mkdir hogehoge

koutarou@koutarou-desktop/INS> docker run --rm -v $(pwd)/hogehoge:/hogehoge docker-permission-test

koutarou@koutarou-desktop/INS> ls -l
合計 8
-rw-rw-r-- 1 koutarou koutarou   57  616 21:26 Dockerfile
drwxrwxr-x 2 koutarou koutarou 4096  616 21:30 hogehoge

koutarou@koutarou-desktop/INS> ls hogehoge -l
合計 0
-rw-r--r-- 1 root root 0  616 21:30 test.txt

hogehogeがroot:rootではなくmkdirしたユーザー・グループ所有のものになっています.

実験その2 〜コンテナで実行するユーザーを指定してみる〜

先程はdocker runするときに特にユーザーを意識しませんでしたが,公式ドキュメントにはコンテナ内で実行されるプロセスの実行ユーザーを指定することができると書いてあります.

先ほどと同じDockerfileでdocker runする時にユーザー指定してみるとどうなるでしょうか?

koutarou@koutarou-desktop/INS> mkdir hogehoge

koutarou@koutarou-desktop/INS> chmod 777 hogehoge

koutarou@koutarou-desktop/INS> docker run --rm -u=1001:1001 -v $(pwd)/hogehoge:/hogehoge docker-permission-test

koutarou@koutarou-desktop/INS> ls -l
合計 8
-rw-rw-r-- 1 koutarou koutarou   57  616 21:26 Dockerfile
drwxrwxrwx 2 koutarou koutarou 4096  616 21:37 hogehoge

koutarou@koutarou-desktop/INS> ls -l hogehoge
合計 0
-rw-r--r-- 1 1001 1001 0  616 21:37 test.txt

docker runするときに1001:1001というユーザー・グループで実行するように指定しました. その結果,test.txtは1001:1001所有になりました.

ドキュメントにはデフォルトではidが0のユーザー(多くの場合はroot)が使われると書いてあります.そのため実験1ではroot:root所有になったと考えられます. そして指定すると指定通りになりました. なお,私のホストマシンにはidが1001のユーザー・グループが存在していないため数字での指定となっています. これを存在する番号で指定すると下のように名前で表示してくれるようになります.

koutarou@koutarou-desktop/INS> docker run --rm -u=117:124 -v $(pwd)/hogehoge:/hogehoge docker-permission-test

koutarou@koutarou-desktop/INS> ls -l hogehoge
合計 0
-rw-r--r-- 1 pulse pulse 0  616 21:41 test.txt

ここで注意してほしいのは,コンテナには117番のユーザーや124番のグループは存在していないのにホストマシンでlsした結果適切な名前で表示されているということです. つまり,コンテナは指定されたら指定の通りの実行ユーザーでファイルシステムにアクセスを行いますが,そのアクセスはホストマシンのファイルシステムにもそのまま適用されるということです. よって,dockerはコンテナとホストマシンの間で特にユーザー・グループの変換を行わず,単純にファイルシステムに番号だけを書き残すということが分かります.

なお,ユーザーの指定はdocker runするときだけでなく,下のようにDockerfileにも書くことができます*3

FROM debian:11-slim
USER 1001:1001

CMD ["touch", "/hogehoge/test.txt"]

実験その3 〜mkdirせずにマウントしたディレクトリに対してユーザー指定したコンテナでアクセスする〜

実験2では手動でmkdirしていましたが,これをdockerに任せるとどうなるでしょうか?ユーザー指定したのでそのパーミッションで作ってくれるのでしょうか?

koutarou@koutarou-desktop/INS> docker run --rm -u=1001:1001 -v $(pwd)/hogehoge:/hogehoge docker-permission-test
touch: cannot touch '/hogehoge/test.txt': Permission denied

koutarou@koutarou-desktop/INS> ls -l
合計 8
-rw-rw-r-- 1 koutarou koutarou   57  616 21:57 Dockerfile
drwxr-xr-x 2 root     root     4096  616 21:57 hogehoge

permission deniedエラーが出てコンテナ実行はエラーに終わりました. また,hogehogeディレクトリはroot:root所有となっています.

実験2までの知見から,「1001番のユーザー」(名前を意識していないのであえてこういう表記にしています)がroot:rootで766なディレクトリに対してファイルを作ろうとしたのでエラーが出たのだ,ということが推測され,恐らく実際その通りです.

ここまでは割と自明なのですが,当初の希望的観測では,ユーザー指定したらそのパーミッションディレクトリを作ってくれるのではとしていました. どうもこれは成り立たなかったようです.

公式ドキュメントによると,マウントするディレクトリが存在しなかった場合にはその都度作られる,としています. 誰が作っているかは書いてなさそうでしたが恐らくdockerの中核であるdocker engineだと思われます. docker engine自体はrootで動いているので新しく作られたディレクトリはroot:root所有になるということです.

勝手にユーザーを推定してくれても親切じゃないかとも思いますが,よく考えてみるとdocker runで指定しないでもDockerfileに書いてイメージ自体に実行ユーザーを設定することもできますし((というかdocker runのuオプション自体それを上書きするという挙動です)),存在しない番号を指定するみたいなこともできてしまうのでdocker engineに任せるというのが自然ですね.

まとめ

  • コンテナは指定されたユーザー番号(デフォルトでは0番のroot)でコンテナ内のアプリケーションを実行する.このユーザーはコンテナ・ホスト共に存在しないものを指定してもよい.
  • コンテナ内でマウントされたホスト上のファイルシステムに書き込むとホスト上でもそのユーザー番号で書き込まれる
  • bind mountで存在しないディレクトリを指定するとdocker engineが勝手に作ってくれ,その所有者はdocker engineの実行ユーザー(多くの場合root)となる

*1:公式ドキュメント的にはbind mountというらしいです.docker管理領域の中に作ってくれるvolumeとは区別されるものです

*2:正確にはunix系コンテナとunix系ホストだと思う.windows系が絡んでくるとどうなるんだろう?

*3:

docs.docker.com

Neovimのdiagnostics.open_float()とlsp.buf.hover()がかぶってしまうのを防ぐ

私がメインエディタとして使っているneovimでは,最近(2022/1にリリースされたv.0.6.1以降)標準でLSP(language server protocol)機能が搭載され,コンパイラ支援でエラー表示・補完などが行えるようになります.

私も最近移行して各種設定をしていたのですが,一つ気になることがありました.今回はその解決法を共有していきたいと思います.

気になった現象

私はエラーや警告などがあった行でカーソルを保持すると,その詳細が表示されるようにしています.

local function on_cursor_hold()
  if vim.lsp.buf.server_ready() then
    vim.diagnostic.open_float()
  end
end

local diagnostic_hover_augroup_name = "lspconfig-diagnostic"
vim.api.nvim_set_option('updatetime', 500)
vim.api.nvim_create_augroup(diagnostic_hover_augroup_name, { clear = true })
vim.api.nvim_create_autocmd({ "CursorHold" }, { group = diagnostic_hover_augroup_name, callback = on_cursor_hold })

カーソル保持で警告がfloating windowが表示される

また,<Leader>lkでカーソル上にあるシンボルをホバーするキーマップを設定しています.

vim.keymap.set('n', '<Leader>lk', vim.lsp.buf.hover, opt)

シンボルのホバー

これらの設定のそれぞれは便利なのですが,エラーがある行でシンボルを表示をするとシンボル表示用のfloating windowが500msだけ表示された後エラー表示用のfloating windowが表示されてしまってシンボル情報をゆっくり見ることができません.

2つの機能によるfloating window同士が衝突してしまう

今回はこれを改善していく方法について書いていきます.

ホバーする際に一時的にエラー表示を無効にする

やり方は他にもあるとは思いますが*1,今回はホバーする際に,一時的にカーソル保持でのエラー表示を無効にする,というやり方で実装します.

local function on_cursor_hold()
  if vim.lsp.buf.server_ready() then
    vim.diagnostic.open_float()
  end
end

local diagnostic_hover_augroup_name = "lspconfig-diagnostic"

local function enable_diagnostics_hover()
  vim.api.nvim_create_augroup(diagnostic_hover_augroup_name, { clear = true })
  vim.api.nvim_create_autocmd({ "CursorHold" }, { group = diagnostic_hover_augroup_name, callback = on_cursor_hold })
end

local function disable_diagnostics_hover()
  vim.api.nvim_clear_autocmds({ group = diagnostic_hover_augroup_name })
end

vim.api.nvim_set_option('updatetime', 500)
enable_diagnostics_hover()

-- diagnosticがある行でホバーをするとすぐにdiagnosticのfloating windowで上書きされてしまうのを阻止する
-- ホバーをしたら一時的にdiagnosticを開くautocmdを無効化する
-- これだけだとそれ以降diagnosticが自動表示されなくなってしまうので有効化するautocmdを一回だけ発行して削除する
local function on_hover()
  disable_diagnostics_hover()

  vim.lsp.buf.hover()

  vim.api.nvim_create_augroup("lspconfig-enable-diagnostics-hover", { clear = true })
  -- ウィンドウの切り替えなどのイベントが絡んでくるとおかしくなるかもしれない
  vim.api.nvim_create_autocmd({ "CursorMoved", "CursorMovedI" }, { group = "lspconfig-enable-diagnostics-hover", callback = function()
    vim.api.nvim_clear_autocmds({ group = "lspconfig-enable-diagnostics-hover" })
    enable_diagnostics_hover()
  end })
end

vim.keymap.set('n', '<Leader>lk', on_hover, opt)

少し複雑ですが,ホバー表示を行うとき(on_hover())に以下のようにしてエラー表示機能の無効化・有効化を行います.

  1. disable_diagnostics_hover()を呼び出してエラー表示を無効化する.これによってシンボル表示がエラー表示によってかき消されることがなくなります.
  2. 1だけだとそれ以降エラー表示がされなくなってしまうのでシンボル表示が終わったら有効化します.
    1. エラー表示機能を有効化した後自身を削除するautocmdを定義する
    2. カーソル移動時にこのautocmdを発火するようにする

こうすると,下のようにホバー表示中はエラーが表示されないようになります.

ホバーがかき消されていない

*1:エラー表示をしようとする時にホバー表示がされているかを確認してから行うなど.最初はこの方向でやろうとしたのですがfloating windowが思ったよりも柔軟に表示位置指定できるので,ホバー用のfloating windowとエラー表示用のfloating windowがかぶるかどうかの判定が難しかったので今回の方向にしました

linuxのシンボリックリンク自体のパーミッションには意味がない

このサイトは移転しました。3秒後に自動でリダイレクトします。

【Rustの所有権システムまとめ 〜その2〜】所有権システム

前回に引き続きRustの所有権システムについての連載をやっていきます. 前回は前提として,rustにおける値と変数の区別について説明しました.

xbgneb0083.hatenablog.com

今回は所有権システムの3つのルールについて説明します.

所有権システムとは

所有権システムは,Rustがメモリ管理を行う際のルールの集まりで,ざっくり言うと全体として「プログラム中のある部分において,メモリ中のある値を束縛することのできる変数は一つだけである」ということを言っています.

より正確に言うと,下の3つのルールから成ります*1

  1. 値は所有者(owner)と呼ばれる変数を持つ
  2. 所有者は同時に1つのみ存在する
  3. 所有者がスコープから外れると値はdropする

3つ目のdropという概念はまだ説明していませんが,これについては次回の記事で説明する予定です. とりあえず今回の記事の範囲では「所有者の変数がスコープから外れると値が無効になって変数を使えなくなる」という理解をしてくれれば十分です.

例1

言葉だけで説明されてもあまり腑に落ちないと思うので,コード例で説明していきます.

fn main() {
    // Box::new()によって値を格納するメモリ領域を動的に確保する
    let x = Box::new(100);
    // ここでは変数xは使える
    println!("{}", x);

    let y = x;

    // アンコメントアウトするとエラー
    // println!("{}", x);
    println!("{}", y);
}

このコードでは変数xに,メモリ中に動的に確保した100という値*2を束縛させています. その後その値を再びyという別の変数に束縛させています.

ここでコード中でコメントアウトされている文をアンコメントアウトすると*3,下のようなエラーが起こります.

error[E0382]: borrow of moved value: `x`
  --> src/main.rs:10:20
   |
3  |     let x = Box::new(100);
   |         - move occurs because `x` has type `Box<i32>`, which does not implement the `Copy` trait
...
7  |     let y = x;
   |             - value moved here
...
10 |     println!("{}", x);
   |                    ^ value borrowed here after move
   |
   = note: this error originates in the macro `$crate::format_args_nl` (in Nightly builds, run with -Z macro-backtrace for more info)

For more information about this error, try `rustc --explain E0382`.

何やらいろいろ言われていますが重要なのは,let y = x;という文で,xが束縛している値の所有権が変数yに移動しているということです. これが1つ目と2つ目のルールが表れている部分で,rustに初めて触れた人は驚くところでしょう.

これらのルールによって,100という値には同時にたった一つの所有者しか存在することができず,結果的に値をyに束縛しようとするとxの束縛は外すしかありません.

ある値を同時に複数の変数が束縛することはできない

3つ目のルールについては次回以降でDropについて説明するときにまた説明します.

例2

先程の例ではただの値ではなくわざわざBoxを使っていたことに気づいた方がいるかもしれません. そこで普通の値を使って先程の例のコードを書き直してみます.

fn main() {
    let x = 100;
    let y = x;
    
    // コメントなしだとエラー...?
    println!("{}",x);
    println!("{}",y);
}

所有権のルールからすると,このコードは先程の例と同じエラーが出てコンパイルできないはずです. しかし,このコードはコンパイルが通ります.

所有権のルールと照らし合わせてみると,値100の所有権はyに移っているはずなのでxをその後使うことはできないはずです. さっきの例と何が違うのでしょうか?

この違いについては次回に説明していこうと思います.

*1:https://doc.rust-lang.org/book/ch04-01-what-is-ownership.html#ownership-rules

*2:正確にはヒープ中に格納された値100を指すBox型の値です

*3:どうでもいいですが今まで「コメントアウト」はコメントを付けることだと思っていましたが逆でした

【Rustの所有権システムまとめ 〜その1〜】値と変数の区別

最近Rustに入門しています. Rustは所有権システムによってメモリ安全性を保証しつつもCやC++と同程度の性能を持つシステムプログラミング言語として最近注目されてきています.

その中で,Rustの一番の特徴である所有権システムについて自分なりにまとめてみようと思いこの記事を書いています.

書こうと思ったら長くなりそうだったので連載という形にしていきます. 今回は,所有権システムの前に前提となる値と変数の区別について書いていきます.

プログラムにおけるデータ

まずは,重要だけど一般にはあまり注意が払われていないデータについて説明していきます.

プログラマが書いたコードには,様々なデータ構造が存在することがほとんどのはずです. 例えば下のコードは,100という値を64bit符号なし整数型(u64)の変数xに可変束縛しています.

let mut x: u64 = 100;

束縛とか難しい用語使ってるけど要は「変数xに100という値を代入」しているんでしょ,と普通は理解すると思います. しかし,ここをもっとちゃんと理解することは個人的には大事だと思っています. というのも,この記事の本編で扱う所有権システムを初め,Rustではデータに関連する概念が多く出てきてあやふやなまま理解しようとしても混乱するからです.

プログラムを実行している時,データはメモリのどこかに置かれます(実際に計算を行うときにはCPUのレジスタに移されますがここでは割愛します).どこかといっているのは,関数の引数などスタック領域にある場合もあればヒープ領域に動的にメモリが確保されることもあるからですが,いずれにせよメモリ中にあることには変わりがないです.このようにメモリに置かれたデータそのもののことは一般に「値(value)」と呼ばれます.

変数

しかし,実際にプログラミングをする際には生の値だけを使っているわけではなく,値を「変数(variable)」に代入して使うことが多いと思います.実際,上のコードでは100という値をxという変数に入れています.このように「値を変数に代入する」,という理解でも間違ってはいないのですが,Rustではもっと的を得た「値を変数に束縛する(bind value to variable)」といった表現が使われます.

この表現では値と変数を明確に別のものと捉えており,ある値をある変数に結びつけることを束縛と表現しています.イメージとしては,あるメモリ領域(及びそこに存在する値)に対して変数が鎖を付けるといった感じでしょうか. また,値が実際にメモリ中に存在しているデータであるのに対し,変数はただ単にあるメモリ領域に名前をつけただけである,ということも重要な理解です.

以上の説明は,下のような図にまとめられます.

値と変数の関係

この図は,例えばこのようなコードがあった時のものです.

let x: usize = 100;
let y: isize = -50;

変数xとyのそれぞれは,100と-50という値を束縛しています. ただし,100や-50といった値は実際にメモリに存在しているのに対し,xやyといった変数はあくまでもプログラム中の概念であり,実行中のメモリにxそのものが存在しているわけではありません*1*2

次回

次回は,いよいよ所有権システムについて説明していきます.

*1:デバッグシンボルなどを含めたバイナリには変数の情報はありますが,あくまでもそれはプログラムと実際の実行の架け橋として存在しているに過ぎません

*2:インタプリタ型言語などランタイムがあるような言語では変数そのものの情報が含まれていることも珍しくはないはずです

【中華蕎麦はざま @茨城県取手市】黒胡椒と背脂の豚そば ~青麻辣醤入り~

茨城県守谷市にある中華蕎麦はざまに行ってきました.

行き方

常総ふれあい道路沿いにあります.目印は江戸川学園取手小学校で,看板のある交差点のすぐそばです.

駐車場の数が少ないので少し奥まったところの路肩に停めました.

f:id:Kotaro7750:20220415172414j:plain
大きな看板

実食

店内に入ってすぐ左にある券売機で食券を買います.

何回も通っている店なので既にレギュラーメニュー(醤油そばと鶏白湯そば)は食べたことがあります. この店は限定メニューが日替わりでメニューに入ります. 限定メニューは来るたびに新しい創作ラーメンが作られていそうなので特に規則性はありません.公式twitterにその日の限定がアップされているので確認すると良いと思います. この日は「黒胡椒と背脂の豚そば ~青麻辣醤入り~」という何やら長い名前のメニューがあったのでこれにしてみます.

f:id:Kotaro7750:20220415172912j:plain
本日の限定看板

青麻辣醤(チンマーラージャン)という聞き慣れない調味料は,青花椒と青唐辛子をブレンドしたものらしいです.

券売機で限定1を買って,食券を渡します(券売機の写真は忘れましたorz). 同時に現金で100円を渡して大盛りにしてもらいます. ラーメン自体は850円だったので計950円です.

しばらくすると着丼しました.

f:id:Kotaro7750:20220415173436j:plain

スープはかなり胡椒の効いたスープです. カエシは醤油だとは思いますが,醤油が霞むくらいヒリヒリします. と思ったのですが,胡椒にしては少し辛すぎるような気がします. もしかしたらこれが青麻辣醤なのかもしれません.

少し細かめにした玉ねぎがいいアクセントになってはいるものの,少しジャンキーな仕上がりになっています.冬だったら香辛料のおかげで暖まると思うんですが(実際に冬には生姜鶏そばというジンジャーなラーメンがあって食べると暖まりました),春で暑い店内だと食べてて少し暑く感じます.

スープとカエシだけだと少しチープになりそうなところを背脂で深みを出していますが,残念ながらあまり好みじゃないスープです.でもあくまでも好みの問題なので美味しいかったことには変わりはありません.

f:id:Kotaro7750:20220415173459j:plain

平打ち麺

麺は醤油そばで選べる平打ち麺とおそらく同じもので,手もみのような縮れが特徴です.手もみのおかげで麺が絡まり合って持ち上げるのに少し苦労はしますが,スープの絡みつき自体はよく,意図的にスープと一緒に食べなくてもスープの味がします. チャーシューは少し筋のある豚チャーシューで,程よく醤油味が染み込んでいます. また,メンマは甘みがあって香辛料がよく効いたスープにはいいアクセントになっています.

美味しいには美味しいのですが,今までのはざまからすると少し異色のラーメンで,あそこまで香辛料を効かせる意図があまり分からなかったので知りたいところです.そんなこと聞いたら失礼ですね