0x90

一回休み

Rust 1.26 新機能まとめ

本サイトを結合したのをおいておきます。

今更ですがRust 1.26が5/10にリリースされました。新機能をおさらいしましょう。今回は相当に大きい変更がたくさん入っています。公式はここです。

impl Trait

impl Traitは最大の変更のひとつでしょう。ざっくりいうと、型名のようにimpl Traitを使えます。

use std::fmt::Debug;

fn hoge(a: impl Debug) {
    println!("{:?}", a);
}

fn fuga() -> impl Debug {
    1
}

引数のトレイト境界を指定する使い方の場合、従来のジェネリクスとの棲み分けをどうするべきなのかはすこし気になります。まあ、簡単な場合であればおそらくimpl Traitを使うようになるんでしょう。ただ、微妙にトリッキーなのは、こういう場合がコンパイルエラーになることです。

trait A {
    fn a(b: i32) -> i32 {
        b + 1
    }
}

struct B;

impl A for B {}

fn hoge<S: A>(a: impl Debug) {
    println!("{}, {:?}", S::a(1), a);
}

fn main() {
    hoge::<B>(3i32);
}

内部的にimpl Traitジェネリクスとして扱われているような感じのエラーメッセージが出てきますが、hoge<B, _>(3i32)などどう書いてみてもコンパイルは通らないので気をつけましょう。

返り値をimpl Traitにした場合、これはジェネリクスとは関係なくその型はあくまでも存在型(existential type)であると書かれています-つまり、impl Aなる返り値は、$\exists T$ s.t. $T: A$なる型$T$の略記です。したがって、これはコンパイルエラーです。

use std::fmt::Debug;

fn piyo(a: i64) -> impl Debug {
    if a == 0 {
        1
    } else {
        "a"
    }
}

結局静的ディスパッチなので自明なのですが、初めて触る人には困惑のもとになる気もします。(またRustの敷居が上がってしまった。。。)

また、複数のトレイト境界を要求する場合は+で結合できます。

fn fuga() -> impl Debug + Display {
    1
}

impl Traitでは実際の型が他のトレイト境界を満たしているかどうかに関わらず-これは静的に決まることではありますが-返された値については明記したトレイトのメソッドしか使えないようになっています。つまり、

fn fuga() -> impl Debug {
    1
}

println!("{}", fuga());

とかはダメなわけです。これをしたければ上のように返り値をimpl Debug + Displayとすることになります。Debug境界も満たしていてほしいなあとか思ったりEqとかPartialEqとか言い出したりするとこの指定がおそらくかなり長ったらしくなりそうですがこれはちょっと鬱陶しいところです。

現状匿名構造体みたいな仕組みはないので、impl Traitをしたところで結局構造体を定義する必要はあります。しかし、今までと異なるのは、定義した構造体が必ずしもパブリックでなくても良いという点です。

mod a {
    use std::fmt::Debug;
    
    #[derive(Debug)]
    struct A;
    
    // OK
    pub fn fuga() -> impl Debug {
        A{}
    }
}

mod b {
    use std::fmt::Debug;
    
    #[derive(Debug)]
    struct A;
    
    // NG
    pub fn fuga() -> A {
        A{}
    }
}

Iteratorの気持ち悪い返り値とかがどうにかなるとよいですね。

追記

公式読んでて気が付きましたが、長ったらしい型名を書かなくて良い場合

fn hoge(v: Vec<i64>) -> impl Iterator<Item=i64> {
    v.into_iter()
     .map(|x| x * 2)
     .filter(|x| x + 4 > 10)
}

や、クロージャを返せる場合(クロージャはそもそも匿名の構造体)

fn hoge() -> impl Fn(i64) -> i64 {
    |x| x * 2
}

とかもありました。特に多分クロージャが静的ディスパッチでよくなったのは本質的な改善点ですね。

match時の適切なref/deref

ちょっと個人的には過保護かなあと思う機能。まあ、理解して使う分には便利かと思います。

fn hello(arg: &Option<String>) {
    match arg {
        &Some(ref name) => println!("Hello {}!", name),
        &None => println!("I don't know who you are."),
    }
}

fn hello(arg: &Option<String>) {
    match arg {
        Some(name) => println!("Hello {}!", name),
        None => (),
    }
}

と書けるようになったという話です。まあこれがないとスライスパターンが(´;ω;`)ウッ…ってなることは目に見えているので仕方ないんでしょうか。

mainがUnit型以外を返せるようになった

main関数が返り値としてResult<(), impl Debug>を返せるようになったようです。

なお、unstable featureとして、std::process::Terminationというtraitがあって、

use std::process::Termination;

impl Termination for Option<()> {
    fn report(self) -> i32 {
        match self {
            Some(())    => 0,
            None        => 1,
        }
    }
}

fn main() -> Option<()> {
    Some(())
}

とも書けるようですが、今バージョンではまだstableで使えないようです。

区間が書けるようになった

Haskellユーザーとかに優しいんじゃないかという機能。

fn main() {
    println!("{:?}", (1..3).collect::<Vec<_>>());
    println!("{:?}", (1..=3).collect::<Vec<_>>());
}

の出力は

[1, 2]
[1, 2, 3]

となります。

なお公式を見るとオーバーフローと組み合わせた例が載っててこれはなるほどなぁという感じ。

スライスパターン

今回の変更でぼくが一番好きなのはコレ。まず基本はmatch文で

fn main() {
    let hoge = [1, 2, 3];
    
    match hoge {
        [a, b, 1] | [1, a, b]   => println!("{}, {}", a, b),
        a @ _                   => println!("{:?}", a),
    }
}

みたいなことができます。ここで注意したいのは、あくまでもスライスパターンはリファレンスを渡しているだけだということです。つまり、1つ目の例なら

fn main() {
    let hoge = [1, 2, 3];
    
    match hoge {
        [ref a, ref b, 1] | [1, ref a, ref b]   => println!("{}, {}", a, b),
        ref a @ _                               => println!("{:?}", a),
    }
}

と等価だと思います。新機能である、match時の適切なrefが効いています。

なお、パターンは以下のように変数束縛やif letでも使えるので、

fn main() {
    let hoge = [1, 2, 3];
    let [a, b, c] = hoge;

    if let [1, 2, d] = hoge {
        println!("{}", d);
    }
}

とかもできます。しかし、スライスパターンからmoveはできないので、Copyトレイトを満たさない型に関してはletすることはできません。

#[derive(Debug)]
struct A {
    a: i64,
}

fn main() {
    let hoge = [A { a : 1 }, A { a : 2 }];
    
    // コンパイルエラー
    let [a, b] = hoge;
}

なお、即値での代入はmove扱いになるようです。

#[derive(Debug)]
struct A {
    a: i64,
}

fn main() {
    let [a, b] = {
        let hoge = [A { a : 1 }, A { a : 2 }];
        hoge
    };
    
    println!("{:?}", a)
}

したがって、上の例でも恒等関数を使うことで無理やり代入するようにすることはできます。

#[derive(Debug)]
struct A {
    a: i64,
}

fn id<T>(v: T) -> T { v }

fn main() {
    let hoge = [A { a : 1 }, A { a : 2 }];
    let [a, b] = id(hoge);
    
    println!("{:?}", a)
}

なお、分配束縛パターンでは、束縛対象をconsumeすることができたので、ちょっとここは気持ち悪いです。

#[derive(Debug)]
struct A {
    a: i64,
}

#[derive(Debug)]
struct B {
    a: A,
}

fn main() {
    let hoge = B { a: A { a: 1 } };
    // これはOK
    let B { a: x } = hoge;

    println!("{:?}", x);
}

また、束縛する対象がスライスの場合、すべての長さを尽くさなくてはいけないので、if letmatchでは使えてもletでは使えません。

fn main() {
    let hoge = &(vec![1, 2, 3])[..];
    
    // OK
    if let [1, 2, d] = hoge {
        println!("{}", d);
    }
    
    // NG コンパイルエラー
    let [a, b, c] = hoge;
}

スライスパターンの導入でリスト操作が相当に楽になると思います。本当は上の恒等関数の例のようにスライスパターンによる代入がもっと簡単にできればベターだったのですが…。