😎

【auto-delegate】Rustで自動委譲を実現するライブラリを作成しました

2023/05/18に公開

このクレートによって指定のトレイトの実装と、その処理を子に委譲させることを自動化できます。

https://crates.io/crates/auto-delegate

https://github.com/elm-register/auto-delegate

委譲の問題点

次のような構造を考えます。

delegate_calc

Calcというトレイトが1つあり、CalcAddがそれを実装しています。
Parentという構造体はCalcAddを保持しています。

Parent自身もCalcを実装しますが、その処理をCalcAddに委譲させる場合コードは次のようになります。

trait Calc {
    fn calc(&self, x1: usize, x2: usize) -> usize;
}

#[derive(Default)]
struct CalcAdd;

impl Calc for CalcAdd {
    fn calc(&self, x1: usize, x2: usize) -> usize {
        x1 + x2
    }
}

#[derive(Default)]
struct Parent {
    child: CalcAdd
}


impl Calc for Parent {
    fn calc(&self, x1: usize, x2: usize) -> usize {
        // ここでChildに処理を委譲します。
        self.child.calc(x1, x2)
    }
}


fn main() {
    let parent = Parent::default();

    assert_eq!(parent.calc(2, 3), 5);
}

Parentのcalcメソッド内では子の処理を委譲させているだけです。
上記のコードではトレイトとメソッドが1つずつしかありませんでしたが、複数になるとどうなるでしょうか?

delegate_calc2

trait Calc {
    fn calc(&self, x1: usize, x2: usize) -> usize;
}


#[derive(Default)]
struct CalcAdd;

impl Calc for CalcAdd {
    fn calc(&self, x1: usize, x2: usize) -> usize {
        x1 + x2
    }
}


trait Movable {
    fn move_to(&mut self, pos: (usize, usize));

    fn pos(&self) -> (usize, usize);
}


trait Resizable {
    fn resize(&mut self, width: usize, height: usize);

    fn size(&self) -> (usize, usize);
}

#[derive(Default)]
struct Transform2D {
    pos: (usize, usize),
    width: usize,
    height: usize,
}


impl Movable for Transform2D {
    fn move_to(&mut self, pos: (usize, usize)) {
        self.pos = pos
    }

    fn pos(&self) -> (usize, usize) {
        self.pos
    }
}


impl Resizable for Transform2D {
    fn resize(&mut self, width: usize, height: usize) {
        self.width = width;
        self.height = height;
    }

    fn size(&self) -> (usize, usize) {
        (self.width, self.height)
    }
}


#[derive(Default)]
struct Parent {
    calculator: CalcAdd,
    transform: Transform2D
}


impl Calc for Parent {
    fn calc(&self, x1: usize, x2: usize) -> usize {
        // ここでChildに処理を委譲します。
        self.calculator.calc(x1, x2)
    }
}


impl Movable for Parent{
    fn move_to(&mut self, pos: (usize, usize)) {
        self.transform.move_to(pos)
    }


    fn pos(&self) -> (usize, usize) {
        self.transform.pos
    }
}


impl Resizable for Parent {
    fn resize(&mut self, width: usize, height: usize) {
        self.transform.resize(width, height)
    }

    fn size(&self) -> (usize, usize) {
        self.transform.size()
    }
}


fn main() {
    let mut parent = Parent::default();

    assert_eq!(parent.calc(2, 3), 5);

    parent.move_to((10, 11) );
    assert_eq!(parent.pos(), (10, 11));

    parent.resize(100, 110);
    assert_eq!(parent.size(), (100, 110));
}

...かなり手間がかかってしまいますね。

着想

上記の問題を解決するために、Kotlinではbyというデリゲートプロパティが用意されています。

interface Calc{
    fun calc(x1: Int, x2: Int): Int
}

class CalcAdd: Calc{
    override fun calc(x1: Int, x2: Int): Int
        = x1 + x2
}

class Parent(private val calculator: Calc) : Calc by calculator



fun main(args: Array<String>) {
    val parent = Parent(CalcAdd())

    // 5
    println(parent.calc(3, 2))
}

このKotlinの機能から着想を得て、同様の処理をRustでも実現できるようにしたものが今回作成したライブラリになります。

使用例

先ほどのコードに対してauto-delegateを適用させたものが以下になります。

use auto_delegate::{delegate, Delegate};

// 委譲対象のトレイトに`delegate`を付与します。
#[delegate]
trait Calc {
    fn calc(&self, x1: usize, x2: usize) -> usize;
}

// ...中略 


// 委譲元に`Delegate`を付与します。
#[derive(Default, Delegate)]
struct Parent<T: Default + Calc> {
    // 委譲先のメンバにトレイト名を指定します。
    #[to(Movable, Resizable)]
    transform: Transform2D,

    // 複数のメンバに対して適用できます。
    // また、ジェネリクスの型にも対応しています。
    #[to(Calc)]
    calculator: T
}

たったこれだけで自動でトレイトの実装と、処理の委譲を行ってくれるので大分コードがスッキリします。
参考までに、手動でトレイトを実装していた個所は以下になります。

impl Calc for Parent {
    fn calc(&self, x1: usize, x2: usize) -> usize {
        // ここでChildに処理を委譲します。
        self.calculator.calc(x1, x2)
    }
}


impl Movable for Parent{
    fn move_to(&mut self, pos: (usize, usize)) {
        self.transform.move_to(pos)
    }


    fn pos(&self) -> (usize, usize) {
        self.transform.pos
    }
}


impl Resizable for Parent {
    fn resize(&mut self, width: usize, height: usize) {
        self.transform.resize(width, height)
    }

    fn size(&self) -> (usize, usize) {
        self.transform.size()
    }
}

Enum

Enumの場合、次のように宣言します。
ただ、ジェネリクスを持つトレイトには対応できていません。

use auto_delegate::{delegate, Delegate};

#[delegate]
trait Calc {
    fn calc(&self, x1: usize, x2: usize) -> usize;
}

#[derive(Default)]
struct CalcAdd;

impl Calc for CalcAdd {
    fn calc(&self, x1: usize, x2: usize) -> usize {
        x1 + x2
    }
}

#[derive(Default)]
struct CalcSub;

impl Calc for CalcSub {
    fn calc(&self, x1: usize, x2: usize) -> usize {
        x1 - x2
    }
}


#[derive(Delegate)]
#[to(Calc)]
enum EnumCalc {
    Add(CalcAdd),
    Sub(CalcSub),
}


fn main() {
    let c = EnumCalc::Add(CalcAdd::default());
    assert_eq!(c.calc(3, 5), 8);


    let c = EnumCalc::Sub(CalcSub::default());
    assert_eq!(c.calc(3, 2), 1);
}

最後に

ライブラリを公開したのは今回が初めてのため、色々至らない点があるかもしれません。

問題点を見つけた方はフィードバックを送ってくださると助かります。

Discussion