🥚

Rustのさわりだけ試したメモ

2022/04/22に公開

前々からやらなきゃと思っていたことの1つとしてRustのさわりだけ見てみました。C/C++との置き換えを目指すもの、とだけ噂を聞いていたので、C++ある程度わかれば分かるでしょと軽い気持ちで始めました。

目標

新しいものを学び始めるにあたって何を目標にするかいつも悩むのですが、以下仕様を満たすVector構造体を作ることにします。

  1. 上階層の他ファイルから読み込める独立したファイル(モジュール)に作る
  2. x,yから構成される2次元ベクトルを表す
  3. 各要素は32bit浮動小数点数とする
  4. 各要素をインデックスで取得できるようにする
  5. オペレータによる四則演算を可能とする
  6. 文字列に整形出力できる

コード

上記の仕様を満たすコードを次に記します。

use std::ops::{Add, Sub, Mul, Div};
use std::result::Result;
use std::fmt::Display;

pub trait VectorF32: {
    fn get_item(&self, index: usize) -> Result<f32, &str>;
    fn get_size(&self) -> usize;

    fn get_length(&self) -> f32 {
        self.get_length2().sqrt()
    }
    fn get_length2(&self) -> f32 {
        let mut result = 0.0;
        for i in 0..self.get_size() {
            let v = self.get_item(i).unwrap();
            result += v * v;
        }

        result
    }
    fn normalized(&self) -> Self;
}

#[derive(Debug, Copy, Clone)]
pub struct Vector2F32 {
    pub x: f32,
    pub y: f32
}

impl VectorF32 for Vector2F32 {
    fn get_item(&self, index: usize) -> Result<f32, &str> {
        match index {
            0 => Ok(self.x),
            1 => Ok(self.y),
            _ => Err("index is out of bounds")
        }
    }
    
    fn get_size(&self) -> usize { 2 }

    fn normalized(&self) -> Self {
        let len = self.get_length();
        return Vector2F32 {
            x: self.x / len,
            y: self.y / len
        }
    }
}

impl Add for Vector2F32 {
    type Output = Vector2F32;
    fn add(self, other: Vector2F32) -> Vector2F32 { 
        Vector2F32 {
            x: self.x + other.x,
            y: self.y + other.y
        }
    }
}

impl Sub for Vector2F32 {    
    type Output = Vector2F32;
    fn sub(self, other: Vector2F32) -> Vector2F32 {
         Vector2F32 {
             x: self.x - other.x,
             y: self.y - other.y
         }
    }
}

impl Mul<f32> for Vector2F32 {    
    type Output = Vector2F32;
    fn mul(self: Vector2F32, other: f32) -> Vector2F32 { 
        Vector2F32 {
            x: self.x * other,
            y: self.y * other
        }
    }
}

impl Mul<Vector2F32> for f32 {    
    type Output = Vector2F32;
    fn mul(self: f32, other: Vector2F32) -> Vector2F32 {
        other * self
    }
}

impl Div<f32> for Vector2F32 {
    type Output = Vector2F32;
    fn div(self: Vector2F32, other: f32) -> Vector2F32 {
         Vector2F32 {
             x: self.x / other,
             y: self.y / other
         }
    }
}

impl Display for Vector2F32 {    
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { 
        write!(f, "{}, {}", self.x, self.y)
    }
}

mod sub;
use sub::vector::Vector2F32;

fn main() {
    let vec = Vector2F32 {
        x: 10.0,
        y: 20.0
    };

    println!("(10.0, 20.0) + (5.0, 5.0) = ({})", vec + Vector2F32{x:5.0, y:5.0});
    println!("(10.0, 20.0) - (5.0, 5.0) = ({})", vec - Vector2F32{x:5.0, y:5.0});
    println!("(10.0, 20.0) * 2.0        = ({})", vec * 2.0);
    println!("2.0 * (10.0, 20.0)        = ({})", 2.0 * vec);
    println!("(10.0, 20.0) / 2.0        = ({})", vec / 2.0);
}

また、ファイル構成は以下のようになります。

- main.rs
- sub.rs
- sub
  - vector.rs

説明

1. ファイル分割

別のファイルで実装されるコードを利用する場合、"mod"でファイルを指定します。main.rsの最初に行っているのがこれです。

mod sub;

「使いたいのはvector.rsなのになんでsubを読むの?」「そもそもsub.rsってなに?」という疑問がありますが、別の階層にあるファイルは直接読み込むことができないようです。
ここでsub.rsですが

pub mod vector;

となっています。ディレクトリと同名のモジュールファイルを作り、そこで階層下のモジュールを読むことで、「このディレクトにあるこのモジュールファイルを使用します」と明示します。そして<ディレクトリ名>.rsを読むことで、間接的に特定ディレクトリ下のモジュールを読み込むようです。
詳しくはクレートのルートモジュールの位置なども関わりそうですが、ひとまずはこの程度の理解でいます。

さて、これでファイルが見えるようになったので、このファイルの中から利用する構造体をuseで指定します。

// subディレクトリの、vectorモジュールにある、Vector2F32を使う
use sub::vector::Vector2F32;

2. x,yから構成される2次元ベクトルを表す

単に、構造体にフィールドを持たせる記述をします。

#[derive(Debug, Copy, Clone)]
pub struct Vector2F32 {
    pub x: f32,
    pub y: f32
}

構造体をpub(public)に指定していますが、これをつけなければ(privateにすれば)他モジュールからは見えなくなり、使えなくなります。またフィールドのx,yそれぞれもpubとしていますが、これをつけなければそれぞれのフィールドが外からアクセス不能となります。

さて、"derive"とはなんだ、という話に進みますが、
これは、指定したtraitを既定の実装でまかなうという指定になります。
traitについては後述しますが、要するにここでは、Vector2F32という構造体に対してDebug、Copy、Cloneの機能を標準実装で提供します。

例えば、C++なんかではコピーコンストラクタを実装しなければ既定の処理が実行されますが、Rustの場合はコピー不可能とされます。コピーを可能にするにはCopyおよびCloneのtraitを実装する必要がありますが、単にフィールドのコピーを行ってくれればいいので標準機能で十分だということです。

3. traitを定義する

Rustでは、データ構造の定義と、処理の実装を分けて記述します。
前項ではデータ構造の定義のみを行いました。処理の実装をするために必要ではないのですが、traitを宣言して、これに沿った実装をします。

まずtraitってなんなの?
本家のマニュアルによると、「共通の振る舞いを定義する」ものと言われていますが、C#経験者としてはinterface+αと言えば近いかなという印象を持ちました。
Vectorに対するinterfaceとして何を持つべきかと考え、(最低限ですが)例のように実装しました。

pub trait VectorF32: {
    // 要素取得
    fn get_item(&self, index: usize) -> Result<f32, &str>;
    // 要素数取得
    fn get_size(&self) -> usize;
    // ノルム取得
    fn get_length(&self) -> f32 {
        self.get_length2().sqrt()
    }
    // ノルムの二乗取得
    fn get_length2(&self) -> f32 {
        let mut result = 0.0;
        for i in 0..self.get_size() {
            let v = self.get_item(i).unwrap();
            result += v * v;
        }

        result
    }
    // 正規化ベクトル取得
    fn normalized(&self) -> Self;
}

メソッドの記法はfn メソッド名(引数) -> 返り値;となります。
いわゆるメンバメソッドの場合は、引数の最初に&selfをとり、メソッドの中でselfを通して自身にアクセスします。また、引数や返り値にSelfを指定することで、このtraitを実装する型を指定することができます。
また、traitはinterfaceとは違い、traitの中で実装を与えることができます。ノルムはベクトルの全要素を取得すれば計算できるので、get_itemが用意されるtraitの時点で実装することができます。

4. traitを実装する

さて、構造体Vector2F32に対し、trait VectorF32を実装します。

impl VectorF32 for Vector2F32 {
    fn get_item(&self, index: usize) -> Result<f32, &str> {
        match index {
            0 => Ok(self.x),
            1 => Ok(self.y),
            _ => Err("index is out of bounds")
        }
    }
    
    fn get_size(&self) -> usize { 2 }

    fn normalized(&self) -> Self {
        let len = self.get_length();
        return Vector2F32 {
            x: self.x / len,
            y: self.y / len
        }
    }
}

traitの実装はimpl trait for struct {}の中で行います。なお、impl struct {}とすることで、traitとは関係なく処理実装ができます。

あまり複雑なことはしていませんが、ひっかかるとすればget_itemでしょうか。
get_itemはインデックスを指定して要素を取得するメソッドです。要素数が2なので、0未満または2以上を指定した場合には例外を投げるようにしたいですが、Rustには例外という概念がありません。代わりに、返り値にエラー情報を含めることができます。
Result<f32, &str>は、エラーがない場合はf32を、エラーがあった場合にはstrの参照を返します。どうしてstrじゃなくて参照なのかって件については省略します。

Add, Sub, Mul, Div traitを実装することで、対応する四則演算オペレータが使用可能になります。
同様にDisplay traitを実装することで、フォーマッタの引数に渡せるようになります。

ここまでで、目標とした仕様は満足できました。

所感

ほんのさわりしか試せていませんが、Rustの雰囲気は少し感じ取れた気がします。
所有権やライフタイムの考え方、また単純に記法にも難しいところがありますが、性能や安全性を考えてのことなんだろうと感じ取れるとこもあります。
個人でのメイン言語に据えるかは置いておくとして、今までだったらC++でやっていたかもしれない実装はRustでやってみるくらいには考えてもいいかと思いました。

Discussion