Rust での Newtype パターンの適用方法について。

基礎知識

Newtype パターンは既存の型をラップし、新しい型として使うデザインパターンである。

なお、Rust で既存の型 Old を新しい型 New で単純にラップするには、sturuct New(Old); と定義すればよい (ラップ先からラップ元へのアクセスは self.0 と記述)。これらは単純なラップのため、最適化によりゼロコストで実装できる。

使用目的

以下は主な Newtype パターンの使用目的。

汎用的な型の特殊化

既存の型が汎用的すぎる場合、Newtype で特殊化するとよい。

例えば、数値型から距離型と質量型を作れば、それらを取り違えるミスは減る。また、それらそれぞれに専用関数も用意できるようになる。

サンプル

以下では、f32 型から Length 型を作り、専用関数 from_km を用意している。


use std::ops::Add;

fn main() {
    let length1 = Length(300.0);
    let length2 = Length::from_km(1.0);
    assert!(length1 + length2 == Length(1300.0));
}

#[derive(PartialEq)]
struct Length(f32);

impl Length {
    pub fn from_km(value: f32) -> Self {
        Self(value * 1000.0)
    }
}

impl Add for Length {
    type Output = Self;

    fn add(self, rhs: Self) -> Self::Output {
        Self(self.0 + rhs.0)
    }
}

完成済の型の再編集

既存の型が完成済で編集できない場合、Newtype で新しい型を作るとよい。

例えば、Rust には孤児ルールがあるため、他クレート由来の型に他クレート由来のトレイトを新たに実装できない。そこで、既存の型へと処理を移譲するだけの新しい型を自クレート内に作成し、このルールに抵触しないようにする。

サンプル

以下では、クレート sub1 の型 SomeStruct にクレート sub2 のトレイト SomeTrait を直接は実装できないため、SomeStructNewtype 型でラップしている。

app/src/main.rs

use sub1::SomeStruct;
use sub2::SomeTrait;

fn main() {
    let some = Newtype::new();
    some.action();
}

pub struct Newtype(SomeStruct);

impl Newtype {
    pub fn new() -> Self {
        Self(SomeStruct::new())
    }

    pub fn do_something(&self) {
        self.0.do_something();
    }
}

impl SomeTrait for Newtype {
    fn action(&self) {
        self.do_something();
    }
}
sub1/src/lib.rs

pub struct SomeStruct();

impl SomeStruct {
    pub fn new() -> Self {
        Self()
    }

    pub fn do_something(&self) {
        println!("Something done.")
    }
}
sub2/src/lib.rs

pub trait SomeTrait {
    fn action(&self);
}

コード量への対処

Newtype パターンは有用だが、ラップ用の単調なコードが長くなりやすい。例えば、数値型では四則演算用の関数のラップなどが必要になる。また、割と大きな型の全ての関数をラップすべきという状況も普通にありうる。

状況によっては下記の対策が使える。それ以外の場合、"Newtype" の名前の通り、既存の型の転用ではなく、新しい型の作成と割り切ったほうが良さそうである。

対策 1. derive 実装

derive 実装できる箇所はまずはそれを使おう。なお、トレイトによっては derive 対応していても適用に追加要件があるが、Newtype パターンでは唯一のフィールドをラップするだけなので、まず問題にならない。

そして、Newtype パターンでのラップを最初から想定している型では、セットでラップ用のトレイトを提供してもよい (derive 対応がかなり面倒だが…)。

対策 2. マクロ

マクロを利用すれば、数値型の四則演算などの普遍的処理は、いくらかコードを短縮できる。ただし、マクロは純粋に構文木変換用のフィルタであるため、たとえ手続型マクロでも入力ソース以外にはアクセスできない。そのため、ラップ対象のメソッドやその引数の情報が得られず、原理的に全自動でのラップは不可能である。

対策 3. Deref 継承

Deref 継承と呼ばれる手法を使えば、本来はスマートポインタのために使われる Deref トレイトを流用して、全処理をラップ先に転送できる。ただし、これはアンチパターンの一種で、あまりお勧めできない。なぜなら、関数がラップ元とラップ先どちらにあるるかは、微妙だが本質的な使用感の違いになる場合があり、混乱を招きやすい。