Rokiのチラ裏

学生による学習のログ

Learning Rust #2

続き

メゾット構文

統一関数呼び出し*1のような記法を言うのかと思いきや、単にメンバ関数を呼ぶ動作とあまり変わらないような気がする。
struct X{
    x:u32,y:u32
}
impl X{
    fn multiple(&self)->u32{self.x*self.y}
    fn create(&self,a:u32,b:u32)->X{X{x:a,y:b}}
}

fn main(){
    let x=X{x:21,y:2};
    assert!(x.create(20,2).multiple()==40);
}
impl内にselfを用いない関数を定義すると、staticとしてメゾットを定義できるようだ。
struct X{
    x:u32,y:u32
}
impl X{
    fn multiple(&self)->u32{self.x*self.y}
    fn create(a:u32,b:u32)->X{X{x:a,y:b}}
}

fn main(){
    assert!(X::create(20,2).multiple()==40);
}
Builderパターンといったイディオムがある。これはデフォルト引数という機能が言語機能として無いながら、言語仕様範囲でデフォルト動作を指定してあるような形態を実現するパターンだ。
struct X{
    x:u32,y:u32
}
impl X{
    fn multiple(&self)->u32{self.x*self.y}
}

struct XBuilder{
    x:u32,y:u32
}
impl XBuilder{
    fn create()->XBuilder{
        XBuilder{x:0,y:0}
    }
    fn first(&mut self,value:u32)->&mut XBuilder{
        self.x=value;
        self
    }
    fn second(&mut self,value:u32)->&mut XBuilder{
        self.y=value;
        self
    }
    fn finalize(&self)->X{
        X{x:self.x,y:self.y}
    }
}

fn main(){
    let a=XBuilder::create().finalize();
    let b=XBuilder::create().first(21).second(2).finalize();
    assert!(a.multiple()==0);
    assert!(b.multiple()==42);
}
単にセットするメンバ関数を定義して、連続的に呼び出せるよう、thisを返し、最後に本質の構造体をインスタンス化して返しているだけだが、組み立て方が少し面白いので、C++でもやってみた。C++でやる意味は本当に全くないのだが。
#include<utility>
#include<cassert>

struct X{
    const std::size_t x,y;
    X(const std::size_t a,const std::size_t b):x(a),y(b){}
    std::size_t multiple()const{return x*y;}
};

struct XBuilder{
    std::size_t x,y;
    XBuilder(const std::size_t a,const std::size_t b):x(a),y(b){}

    static XBuilder create()
    {
        return XBuilder(0,0);
    }
    XBuilder& first(const std::size_t x)
    {
        this->x=x;
        return *this;
    }
    XBuilder& second(const std::size_t y)
    {
        this->y=y;
        return *this;
    }
    X finalize()
    {
        return X(x,y);
    }
};

int main()
{
    const auto c=XBuilder::create().first(20).second(2).finalize();
    const auto d=XBuilder::create().finalize();

    assert(c.multiple()==40);
    assert(d.multiple()==0);
}
ただこのBuilderパターンには一つ欠点がある。それは、単にデフォルト設定を用いたいだけなのに、本質オブジェクト生成用のビルダーオブジェクトが生成されてしまう事である。また、引数が増える度に、メンバ関数も増え続けなければならない。こうなると、デフォルト設定をrustで行うというのは正直あまり気が乗らないところがある。

ベクタ

let v=vec![1,2,3,4,5];
let mut i=0;
for x in v{
    i+=1;
    assert!(x==i);
}
インデックスに用いるtypeはusizeのみに制限されている。
let v=vec![1,2,3,4,5];
let mut i:i32=0;
for (x,y) in (0..v.len()).enumerate(){
    i=x; // usize typeなので代入できない。
    assert!(v[i]==y+1);
}
イテレートする際に、アクセス方法を従来通り指定できる。
let mut v:Vec<i32>=vec![1,2,3,4,5];
for i in &v{ // immutable reference
    println!("{}",i);
}
v[0]=42; // no problem

for i in &mut v{ // mutable reference
    *i=42;
}

for i in v{ // immutable
    println!("{}",i);
}
v[0]=42; // ill-formed. V was moved.

文字列

&strとString
&strは静的にアロケートされるUTF-8バイトシーケンスへの参照である。
let greeting="Hello there"; // greeting: &'static str
文字列リテラルは複数行に渡ることができる。*2
fn main(){
    let s="foo
    bar";
    let s1="foo\
    bar";
    assert_eq!("foo\n    bar",s);
    assert_eq!("foobar",s1);
}
StringはヒープにアロケートされるUTF-8バイトシーケンスである。
let mut s:String="Hello".to_string();
s.push_str(",hoge");
assert_eq!("Hello,hoge",s);
インデクシング/ランダムアクセス
双方とも、UTF-8バイトシーケンス文字列のためインデクシング/ランダムアクセス操作は必然的にサポートされていない。
let s="hello";
println("{}",s[0]); // ill-formed
そこでcharsリストへ変換し線形アクセスする事により同機能を実現する事ができる。(unwrap関数については後述している)
let hachiko="忠犬ハチ公";
assert!(hachiko.chars().nth(0).unwrap_or('0').eq(&"忠".chars().nth(0).unwrap_or('0')));
しかし、場合によって*3このアクセス方法はとても非効率的となる可能性がある。
そういった場合は、ランダムアクセス対応なものを新たに生成しても良いかもしれない。
let hachiko="忠犬ハチ公";
assert!(hachiko.chars().collect::<Vec<char>>()[hachiko.chars().count()-2].eq(&"チ".chars().nth(0).unwrap()));
スライス/結合
スライス構文によって文字列のスライスができる。
let hachiko_eng="chuken_hachiko";
assert!("hachiko"==&hachiko_eng[hachiko_eng.find('_').unwrap()+1..hachiko_eng.len()]);
selfがStringでかつムーブされても良い場合、+演算子によって結合する事ができる。またStringを引数に取る場合、&strに型強制される。
let hello="Hello".to_string();
let world="world";
assert_eq!("Helloworld",hello+world); // helloはムーブされる
assert_eq!("Helloworld","Hello".to_string()+&world);
文字列リテラル同士の場合concat!()リテラル以外の場合format!()で成形する。
let hello="Hello";
let world="world";
assert_eq!(format!("{}{}",hello,world),concat!("Hello","world"));
同じくリテラル同士の場合、joinconcatでも結合操作が可能である。
let s=["忠犬","ハチ公"];
assert_eq!("忠犬 ハチ公",[s[0],s[1]].join(" "));
assert_eq!("忠犬ハチ公",[s[0],s[1]].concat());
よく使うイディオム
  • 文字数カウント。
    let hachiko="忠犬ハチ公";
    assert_eq!(5,hachiko.chars().count());
    
  • split。
    let hachiko_chr="忠.犬.ハ.チ.公".split('.');
    for (s,x) in hachiko_chr.enumerate(){
        assert_eq!("忠犬ハチ公".chars().nth(s).unwrap().to_string().as_str(),x);
    }
    let hachiko_chr="忠。犬。ハ。チ。公".split("。");
    for (s,x) in hachiko_chr.enumerate(){
        assert_eq!("忠犬ハチ公".chars().nth(s).unwrap().to_string().as_str(),x);
    }
    
  • 置換
    assert_eq!("忠犬ハチ公","忠犬ロキ公".replace("ロキ","ハチ"));
    
  • 部分文字列が含まれているか検索する。
    let hachiko="忠犬ハチ公";
    assert!(hachiko.starts_with("ハチ"));
    assert!(!hachiko.starts_with("八"));
    assert!(hachiko.contains("忠犬"));
    

unwrap, ..._or, ..._or_else

内包されている値の型 Tをラップしているデータ(OptionやResultなど)から、内包している値が存在する場合はその値を返し、値が存在しない場合panic!を呼び出すメゾットである。
fn unwrap<T>(x:Option<T>)->T{
    match x{
        Option::Some(val)=>val,
        Option::None=>panic!("call panic"),
    }
}

fn main(){
    let mut a:Option<u32>=Some(42);
    assert_eq!(42,unwrap(a));
    a=None;
    unwrap(a); // panic will fire
}
..._orや..._or_elseは、値が存在しない場合何を返すか指定できる関数である。指定する値の型は内包されている値の型 Tと同じ型でなければならない。
assert_eq!(Some("hoge").unwrap(),"hoge");
assert_eq!(None.unwrap_or("empty"),"empty");
assert_eq!(Some(42).unwrap_or_else(|| 0),42);
assert_eq!(None.unwrap_or_else(|| 0),0);

ジェネリクス

ジェネリクスは表題通りジェネリックな関数、データ型を定義できる機能であり、ジェネリクスを成し遂げる型推論パラメトリックポリモーフィズムと呼ばれるんだとか。(そのまんま)

トレイト

traitキーワードによってインタフェースのような、ある型が提供しなければならない機能を表現する事ができる。書式は以下のように。
struct X{
    x:u32,
    y:u32
}
trait Sum{
    fn sum(&self)->u32;
}
impl Sum for X{
    fn sum(&self)->u32{x+y}
}
そしてジェネリック関数においてトレイトの境界を明確に記述できるという。
この機能は、T型に対してhoge()というメゾットを要求する関数f()において、hoge()というメゾットを持っているデータ型にのみパラメトリックポリモーフィズムを発動するといった大変素晴らしい機能である。*4
コードで表すとこうだ。
fn sum_any<T>(a: T)->u32{
    a.sum();
}
sum_anyはT型に対してsum()メゾットを要求する関数である。しかし、T型が必ずしもsum()メゾットを実装しているかは保証されていない。
そこで、このように書く。
fn sum_any<T: Sum>(a: T)->u32{
    a.sum();
}
T型に対して指定する形で、sum()メゾットを実装しているtrait名を記述する事で、「Sumトレイトを実装するあらゆる型」といったセマンティックとなる。
trait Sum{
    fn sum(&self)->u32;
}
struct X{
    x:u32,
    y:u32,
}
impl Sum for X{
    fn sum(&self)->u32{self.x+self.y}
}

fn sum_any<T:Sum>(x:T)->u32{x.sum()}

fn main(){
    assert_eq!(sum_any(X{x:21,y:21}),42);
}
C++で同じような意味合いのコードを書くとすれば、まあせいぜい以下のようになるだろう。
#include<utility>
#include<cassert>
#include<type_traits>

struct Base{
    using sum_trait=Base;
    virtual std::size_t sum()const=0;
};

struct Derived final:Base{
    Derived(const std::size_t& x,const std::size_t& y):x(x),y(y){}
    std::size_t sum()const override final{return x+y;}
    std::size_t x,y;
};

template<class _Tp>
auto sum_any(_Tp&& x)
->decltype(std::void_t<typename _Tp::sum_trait>(),
        typename std::enable_if<std::is_abstract<typename _Tp::sum_trait>::value>::type(),
        std::size_t())
{
    return x.sum();
}

int main()
{
    assert(42==sum_any(Derived(21,21)));
}

続く。

参照

*1:http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0301r1.html

*2:Vimプラグイン(wting/rust.vim)によって改行すると4スペース分のインデント処理が付与される

*3:最後から1つ手前の要素に何度もアクセスするなど

*4:誤解を恐れずに言えば、C++のSFINAEによって実現している機能(void_tやdecltypeを用いた有効判定文など)のうちの1つと同等である。