Rust structとenumの組み合わせ
Shiikaのctx周りをリファクタリングしたので、何をやったかメモしておく。
元のコード
リファクタリングしたのはHirMakerContextという構造体である。こいつはASTからHIR(高レベル中間表現)を作る際に使うもので、もとはこういう定義だった。
#[derive(Debug)]
pub struct HirMakerContext {
/// Type of this ctx
pub kind: CtxKind,
/// Where this ctx is in the ctx_stack
pub depth: usize,
/// Signature of the current method (Used to get the list of parameters)
/// None if out of a method
pub method_sig: Option<MethodSignature>,
/// The type of current `self`
pub self_ty: TermTy,
/// Names of the type parameter of the current class (or method, in the future)
pub typarams: Vec<String>,
/// Current namespace
/// `""` for toplevel
pub namespace: ClassFullname,
/// Current local variables
pub lvars: HashMap<String, CtxLVar>,
/// List of free variables captured in this context
pub captures: Vec<LambdaCapture>,
//
// ivar-related stuffs
//
/// List of instance variables in an initializer found so far
pub iivars: SkIVars,
/// Number of inherited ivars. Only used when kind is Initializer
pub super_ivars: SkIVars, // TODO: this can be just &'a SkIVars
}
何が問題かというと、項目が不必要に多い。kindはToplevel/Class/Method/Initializer/Lambdaの5種類を取るのだが、それらで使う項目が全部ここに入ってしまっている。たとえばcapturesはLambdaでしか使わないし、iivarsはInitializerでしか使わない。
リファクタ案1
これを整理してみたのが以下。CtxDetailというenumを定義して、ある場面でしか使わない項目はそこに移動した。どうだろうか?
#[derive(Debug)]
pub struct HirMakerContext {
/// Type of this ctx
pub kind: CtxKind,
(中略)
/// Additional information
pub detail: CtxDetail,
}
#[derive(Debug)]
pub enum CtxDetail {
Toplevel,
Class,
Method {
/// Signature of the current method
signature: MethodSignature,
},
Initializer {
/// List of instance variables in an initializer found so far
iivars: SkIVars,
/// List of inherited ivars
super_ivars: SkIVars, // TODO: this can be just &'a SkIVars
/// Signature of the current method
signature: MethodSignature,
},
Lambda {
/// List of free variables captured in this context
captures: Vec<LambdaCapture>,
/// Signature of the current method
signature: MethodSignature,
},
}
リファクタ案1の問題点
一見すっきりして良いように見えるが、実際にコードを書いてみるとどうも不便なことに気づいた。元のコードだとctxのcapturesを参照するには
ctx.captures
と書くだけで良かったが、修正後はいちいち
if let CtxBody::Lambda { captures, .. } = &self.body {
&captures
} else {
panic!("this ctx is not Lambda")
}
のように条件分岐を挟まなければいけない。このようにヘルパーメソッドを書いてみたものの、structの要素ごとにこれをやっていたのでは大量のヘルパーメソッドができてしまいそうだ。
せめてctxの種類だけヘルパーメソッドを作ることができればまだましなのだけど、Rustではenumのvariant(ここでいうCtxBody::Lambdaなど)は型ではないので、「CtxBody::Lambdaを返すメソッド」を定義することはできない。
リファクタ案2
だとしたら、それぞれのvariantをstructにすればいいのか?struct -> enum -> structという構成。
#[derive(Debug)]
pub struct HirMakerContext {
/// Type of this ctx
pub kind: CtxKind,
(中略)
/// Additional information
pub detail: CtxDetail,
}
#[derive(Debug)]
pub enum CtxDetail {
Toplevel(TopLevelCtx),
Class(ClassCtx),
Method(MethodCtx),
Lambda(LambdaCtx)
}
pub struct MethodCtx {
/// Signature of the current method
signature: MethodSignature,
}
...
これも参照するときに条件分岐が必要なのは変わらないが、種類ごとに以下の3つのヘルパーメソッドを用意すればだいぶましになりそう。
// スタックトップをLambdaCtxとしてpopする
pub fn pop_lambda_ctx(&mut self) -> LambdaCtx {
if let HirMakerContext_::Lambda(c) = self.ctx_stack.pop().unwrap() {
return c
} else {
panic!("stack top is not LambdaCtx");
}
}
// スタックトップをLambdaCtxとしてreadonlyな参照を返す
pub fn latest_lambda_ctx(&self) -> Option<&LambdaCtx> {
for item in self.ctx_stack.iter().rev() {
if let HirMakerContext_::Lambda(c) = item {
return Some(c)
}
}
None
}
// スタックトップをLambdaCtxとしてmutableな参照を返す
pub fn latest_lambda_ctx_mut(&mut self) -> Option<&mut LambdaCtx> {
for item in self.ctx_stack.iter_mut().rev() {
if let HirMakerContext_::Lambda(c) = item {
return Some(c)
}
}
None
}
リファクタ案3
いろいろ考えているうちに、「一部だけ違うstructを一つの配列に突っ込んでいるのがだめなのかもしれない」という考えに至った。スタックを一本ではなく、種類ごとに用意するのはどうだろうか?
#[derive(Debug)]
pub struct HirMakerContext {
pub toplevel: ToplevelCtx,
pub classes: Vec<ClassCtx>,
pub method: Option<MethodCtx>,
pub lambdas: Vec<LambdaCtx>,
}
#[derive(Debug)]
pub struct ToplevelCtx {}
#[derive(Debug)]
pub struct ClassCtx {}
#[derive(Debug)]
pub struct MethodCtx {}
#[derive(Debug)]
pub struct LambdaCtx {
/// List of free variables captured in this context
pub captures: Vec<LambdaCapture>,
}
というのを実装したのが冒頭のこのプルリクである。すっきりはしたものの、これにも少し問題がある。このあと5番目の種類としてWhileを入れたいのだが、whileとlambdaは相互にネストし得るので、スタックを分けることができないのだ。一応むりやり回避する案はあるものの、どうするかなー。