【Rust RFC 3058: Try Trait v2】?演算子(Question Operator)の新しい挙動についてのまとめ
この記事では現在RustのRFC3058として提案されている,Try Trait v2について解説していきます.
とはいっても私はまだRustを学習している段階で,RFCの背景や実際になされている議論などには踏み込んだ説明をすることはできませんのでそこはご了承ください. もっと詳しく知りたいという方はRFCのページを見ると全て書いてあります.
このRFCは今も議論が続けられているのでもしかしたら大幅に変わる可能性もあります.ご了承ください.
また,v2という名前から察する人もいるかもしれませんが,同じ目的のv1もありました(今はv2に置き換えられています). rust-lang.github.io そちらについては書かないのか,と思う方もいるかもしれませんが,機能の説明にあたってはv1との差分をそこまで気にする必要はなさそうだと判断したため省いています. 実際のAPIのデザインに当たってはv1との差分を考えているようでしたので興味がわいた方は調べてみると面白いと思います(そしてできれば私に教えてください).
?演算子の現在の挙動
このRFC(以下try_trait_v2)は,?演算子の挙動を新しくするという目的で提案されています.
そこでまずは?演算子の現在の挙動についておさらいしていきます.
The Rust Programming LanguageやReferenceに書かれているように,?演算子は「評価するとResult<T,E>やOption<T>になるような式の後ろにつけることで,OkやSomeならTと評価され,ErrやNoneならその関数から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::Errorやstr::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つのトレイトや型を用いて?演算子を実装します.
- ops::ControlFlow型
- ops::FromResidualトレイト
- ops::Tryトレイト
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がスーパートレイトになっていたりしますが,重要なのは二つの関連型OutputとResidual,branch関数です.
OutputとResidual
この二つの関連型はそれぞれ「処理を継続するときの値」と「早期リターンするときの値」に対応します.
分かりやすいのはOutputの方で,let x = expression?としたとき,xの型はOutputとなる,といった具合なので特に説明はしません.
少しわかりにくいのがResidualの方で,こちらはそもそも単語レベルでなじみがありませんが*5,単語の意味としては「残余」というのがメジャーらしいです.
こちらはOutputとは逆に早期リターンするときの値の素になる値の型です.少しお茶を濁したような言い方をしている理由はFromResidualの説明で分かると思いますので,ここではリターンされる値なんだなという理解で十分です.
Residualという分かりにくい単語ではありますがイメージとしては,「正常」な処理による値(Output)の残りが「エラー」でリターンされる値(Residual)という感じでしょうか(イメージを分かりやすくするため,早期リターン=エラーという前提に立ちましたが前述のとおり正しくない使い方です).
まとめると,?演算子によって最終的に処理の中心となる値の型のうち,処理継続の時に使われるのがOutputで,早期リターンで使われるのがResidualという型ということです.
branch関数
?によってOutputやResidualといった型の値に処理の対象が分岐していきますが,どのような時に処理を続けてどのような時に早期リターンするのか,というのを決定するのが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)というように解釈されるため,StatusのResidualであるStatusResidual型からcall_fn_and_return_result関数の返り値であるResult<(),NonZeroUsize>への変換が存在する必要があるためです.
最後に
とても長くなりましたが,自分なりにRFC 3058のまとめのようなものを書いてみました.
記事を書くのにあたって,rustのRFCやそのtracking issueを初めて(完全には追えてはいないものの)読んでみましたが,当初とはデザインが変わっているという旨がかかれていたり,型の名前があまり直感的ではないので直した方がよいという議論が上がっていたりとRustはこうやって作られているのだな,というのを少し体感できたような気がして,いつかRust本体にも関わってみたいなと思いました.
dockerコンテナ内でホストからマウントされたファイルシステムにアクセスするときのパーミッションについて
このサイトは移転しました。1秒後に自動でリダイレクトします。
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 })

また,<Leader>lkでカーソル上にあるシンボルをホバーするキーマップを設定しています.
vim.keymap.set('n', '<Leader>lk', vim.lsp.buf.hover, opt)

これらの設定のそれぞれは便利なのですが,エラーがある行でシンボルを表示をするとシンボル表示用のfloating windowが500msだけ表示された後エラー表示用の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())に以下のようにしてエラー表示機能の無効化・有効化を行います.
disable_diagnostics_hover()を呼び出してエラー表示を無効化する.これによってシンボル表示がエラー表示によってかき消されることがなくなります.- 1だけだとそれ以降エラー表示がされなくなってしまうのでシンボル表示が終わったら有効化します.
- エラー表示機能を有効化した後自身を削除するautocmdを定義する
- カーソル移動時にこのautocmdを発火するようにする
こうすると,下のようにホバー表示中はエラーが表示されないようになります.

*1:エラー表示をしようとする時にホバー表示がされているかを確認してから行うなど.最初はこの方向でやろうとしたのですがfloating windowが思ったよりも柔軟に表示位置指定できるので,ホバー用のfloating windowとエラー表示用のfloating windowがかぶるかどうかの判定が難しかったので今回の方向にしました
linuxのシンボリックリンク自体のパーミッションには意味がない
このサイトは移転しました。1秒後に自動でリダイレクトします。
【Rustの所有権システムまとめ 〜その2〜】所有権システム
前回に引き続きRustの所有権システムについての連載をやっていきます. 前回は前提として,rustにおける値と変数の区別について説明しました.
今回は所有権システムの3つのルールについて説明します.
所有権システムとは
所有権システムは,Rustがメモリ管理を行う際のルールの集まりで,ざっくり言うと全体として「プログラム中のある部分において,メモリ中のある値を束縛することのできる変数は一つだけである」ということを言っています.
より正確に言うと,下の3つのルールから成ります*1.
- 値は所有者(owner)と呼ばれる変数を持つ
- 所有者は同時に1つのみ存在する
- 所有者がスコープから外れると値は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型の値です
【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.
次回
次回は,いよいよ所有権システムについて説明していきます.
【中華蕎麦はざま @茨城県取手市】黒胡椒と背脂の豚そば ~青麻辣醤入り~
行き方
常総ふれあい道路沿いにあります.目印は江戸川学園取手小学校で,看板のある交差点のすぐそばです.
駐車場の数が少ないので少し奥まったところの路肩に停めました.

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

青麻辣醤(チンマーラージャン)という聞き慣れない調味料は,青花椒と青唐辛子をブレンドしたものらしいです.
券売機で限定1を買って,食券を渡します(券売機の写真は忘れましたorz). 同時に現金で100円を渡して大盛りにしてもらいます. ラーメン自体は850円だったので計950円です.
しばらくすると着丼しました.

スープはかなり胡椒の効いたスープです. カエシは醤油だとは思いますが,醤油が霞むくらいヒリヒリします. と思ったのですが,胡椒にしては少し辛すぎるような気がします. もしかしたらこれが青麻辣醤なのかもしれません.
少し細かめにした玉ねぎがいいアクセントになってはいるものの,少しジャンキーな仕上がりになっています.冬だったら香辛料のおかげで暖まると思うんですが(実際に冬には生姜鶏そばというジンジャーなラーメンがあって食べると暖まりました),春で暑い店内だと食べてて少し暑く感じます.
スープとカエシだけだと少しチープになりそうなところを背脂で深みを出していますが,残念ながらあまり好みじゃないスープです.でもあくまでも好みの問題なので美味しいかったことには変わりはありません.

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