正文
pub struct ExDataFrameRef(pub RwLock);
#[derive(NifStruct)]
#[module = "ExPolars.DataFrame"]
pub struct ExDataFrame {
pub inner: ResourceArc,
}
这样一个结构,两边来回拷贝,也就几十个字节,无伤大雅,但满足了我能够很方便地在 elixir 侧进行 inspect 的需求(就好像完整的数据在 elixir 侧一样)。我只需实现
Inspect
接口即可:
defimpl Inspect, for: ExPolars.DataFrame do
alias ExPolars.Native
def inspect(data, _opts) do
case Native.df_as_str(data) do
{:ok, s} -> s
_ -> "Cannot output dataframe"
end
end
end
于是,当一个 dataframe 被读取出来时,在 Elixir 里,其展现是非常友好的,和 pandas 一个水准:
Well done, buddy。有什么比 elixir 和 rust 两边写了几个函数就得到了这样一个沁人心脾的结果更美妙的呢?嗯,完美的开局意味着美好的结局,我对自己说。
第一次撞墙:RAII
作为一个很懒惰的程序员,在不断遇到重复的代码 pattern 时,想到的第一件事就是 DRY (Don't Repeat Yourself)。很快,我发现在 Rust 侧要访问 dataframe,自己总要写这样的代码:
match data.inner.0.read() {
Ok(df) => deal_with_df,
Err(_) => Err(ExPolarsError.Internal(...))
}
没办法,因为在 rust 侧拿到引用后,需要取读锁(或者写锁),然后才能做相应的操作。于是我想到了重构。可能是之前完美的开局让我有些飘飘然,我想也没想大手一挥,把上述代码封装到一个函数里:
def get_reader(*self) -> DataFrame {
match data.inner.0.read() {
Ok(df) => &*df,
Err(_) => Err(ExPolarsError.Internal(...))
}
}
然后喜滋滋就这么一个函数一个函数地写下去,直到。。。Rust 编译器教我做人。
我才意识到读写锁返回的是一个特殊的 Result:
Result
, PoisonError
>>
,它是 RAII 结构 [6](有关 RAII 的介绍请自行 wikipedia,否则这篇文章不用写别的了),所以我必须在同一个上下文中处理(不同的上下文意味着并发下的重入问题,rust 编译器帮我们杜绝了这种情况导致的死锁)。这是我遇到的第一个 brick wall。我的「人生导师」Randy Pausch 说:
我不想放弃我 DRY 上的努力,但封装函数此路不同,怎么办?
还好,rust 有 macros。于是,几经探索后,我写下了这段代码:
macro_rules! df_read {
($data: ident, $df: ident, $body: block) => {
match $data.inner.0.read() {
Ok($df) => $body,
Err(_) => Err(ExPolarsError::Internal(...)),
}
};
}
这就是本文开头那段示范代码中函数
df_add
里使用了奇怪的
df_read!
。这让我每个函数少些很多重复的代码,最大程度让 Rust 编译器满意,并且使我的代码足够 DRY。