Rokiのチラ裏

学生による学習のログ

Learning Rust #1

The Rust Programming Languageを順番に読みながら学んでいくログをこちらにまとめてみる事とした。尚、シンタックス、セマンティックスの捉え方において、同様な意味合いとしていくつかC++コードを例題として挙げるかもしれない。その辺りは予めご了承ください。
また、当連載エントリは2015/5/15にリリースされたRust 1.0準拠です。

導入からビルド、実行

導入。

% curl -sSf https://static.rust-lang.org/rustup.sh | sh
% rustc --version && cargo --version

新規プロジェクト作成。*1

% cargo new new_project --bin

ビルド、実行。

% cargo build
% ./binary_file

or

% cargo run

リリースビルドは以下のようにする。

% cargo build --release

Vimの設定

NeoBundle 'wting/rust.vim'

を記述して:NeoBundleinstallする。

ミュータブルとイミュータブル、変数束縛

再束縛という概念がある。再束縛する事でimmutableにしたり、mutableにできる。
let x:i32=42; // immutable
let mut x=x; // mutable
x=0;

let mut y:i32=42; // mutable
y=7;
let y=y; // immutable
再束縛は型の変動を許容する。
let y:i32=42;
let y:&str="hoge"; // 再束縛
静的言語であるので異なる型同士の代入は許容されない。
let mut y:&str="hoge";
y=42; // ill-formed

シャドーイング

シャドーイングの関係。分かりやすくするため、同セマンティックスであるC++コードを示す。
fn main(){
    let mut x:i32=42;
    let y:i32=42;
    {
        x=50;
        let y=50;
    }
}

/*
 int x=42;
 const int y=42;
 {
    x=50;
    const int y=50;
 }
this code is cpp
*/

関数のプロトタイプ宣言

は、必要ないようだ。
fn main(){
    print_sum(21,21);
}

fn print_sum(l:i32,r:i32){
    println!("{}",l+r);
}

式と文

Cライクな文法を好んだ者はrustの式と文の扱いに興味深く思うかもしれない。
fn sum(x:i32,y:i32)->i32{
    return x+y;
}
この記述は、コンパイルが通る。しかし、この記述は、式を重んじるrust的ではないためdeprecateとされている。文を用いない記述法の一つとして以下のように記述する。
fn sum(x:i32,y:i32)->i32{
    x+y;
}
しかし、この記述はill-formedである。返す型が異なるからだ*2。Cライクな文法を好むものは、式の末尾にセミコロンを付けたものが文であると認識できるので、この記述をwell-formedと考えるが、これはrustでは、文を返すというセマンティックとなるため、()を返す、即ち空を返すというセマンティックとなる。そのため返却値から推測される型が異なるというエラーを吐くのである。
上のコードを以下のように変更する事でwell-formedとはなる。
fn main(){
    println!("{}",()==sum(21,21)); // true
}

fn sum(l:i32,r:i32)->(){
    l+r;
}
しかし、これは空を返しているので、多くの場合、意図した動きではない。
式にする事で多くの場合に意図される通りに動く。
fn sum(l:i32,r:i32)->i32{
    l+r
}

早期return

rustのreturnは早期に値を返却するためだけにあるといっても過言ではない。
fn main(){
    println!("{}",sum(21,21));
}

fn sum(l:i32,r:i32)->bool{
    if l+r==42{return true;}else{false}
}

ダイバージング

クラッシュを起こすマクロが定義されている。またその関数はクラッシュされるので返却型を持たない。そのような場合にダイバージと読むエクスクラメーションマークを用いる。
fn main(){
    diverges();
}

fn diverges()->!{
    panic!("clush");
}
実行結果の詳細を見たければ、以下のようにする事で詳細情報が出力される。
% RUST_BACKTRACE=1 ./main 
thread 'main' panicked at 'clush', main.rs:6
stack backtrace:
   1:        0x105a5177b - std::sys::backtrace::tracing::imp::write::h46e546df6e4e4fe6
   2:        0x105a52c9a - std::panicking::default_hook::_$u7b$$u7b$closure$u7d$$u7d$::h077deeda8b799591
   3:        0x105a528ca - std::panicking::default_hook::heb8b6fd640571a4f
   4:        0x105a4e148 - std::panicking::rust_panic_with_hook::hd7b83626099d3416
   5:        0x105a4ce97 - std::panicking::begin_panic::hf17b963d1d3f11f5
   6:        0x105a4cde9 - main::diverges::h929fafbd2a534a1e
   7:        0x105a4cda8 - main::main::he714a2e23ed7db23
   8:        0x105a524ad - std::panicking::try::call::hca715a47aa047c49
   9:        0x105a5334b - __rust_try
  10:        0x105a532e5 - __rust_maybe_catch_panic
  11:        0x105a522d1 - std::rt::lang_start::h162055cb2e4b9fe7
  12:        0x105a4ce19 - main

関数ポインタ

関数ポインタを格納する事ができる。便利だ。
fn sum(x:i32,y:i32)->i32{
    x+y
}

fn main(){
    let f:fn(i32,i32)->i32=sum;
    println!("{}",f(21,21));
}

配列

let a:[i32;3]=[1,2,3];
let b:[i32;20]=[0;20]; // 0 initializing
println!("{}",a[a.len();]); // index out of bounds
20要素全てを0で初期化できたり、要素数を取得できるプロパティがある。また、配列アクセスの際に境界チェックを受けるので、境界を超えてのアクセスを記述すると以下のようなエラー文と共に終了する。
% ./main
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 3', main.rs:5
note: Run with `RUST_BACKTRACE=1` for a backtrace.

スライス

let a:[i32;5]=[1,2,3,4,5];
let strings:[&str;5]=["hoge","foo","bar","fuga","piyo"];

let complete=&a[..];
let middle=&a[0..3];
let str_complete=&strings[..];
let str_middle=&strings[0..3];

assert!(complete==a);
assert!(middle==[1,2,3]);
assert!(str_complete==strings);
assert!(str_middle==["hoge","foo","bar"]);

タプル

let mut x:(i32,&str)=(42,"hoge");
let y=(21,"foo");
let (a,b,c,d)=(1,2,"hoge","foo");
x=y;
assert!(x.0==21);
assert!(x.1=="foo");

if式

文ではない。式である。
let x:u32=0;
let x:u32=if x==0{42}else{21};
assert!(x==42);

ループ記法

無限ループが意図されているのであればwhileではなくloop式を用いるべきである。while式で無限ループを実現した場合よりもloop式を用いた方が安全面の高い最適化されたコードを生成する。
let mut x:u32=0;
loop{
    if x>10{break;}
    x+=1;
}

let mut b:bool=false;
x=5;
while !b{
    x+=x-3;
    if x%5==0{b=true;}
}
for式。
let mut x:u32=1;
for i in 0..10{
    x+=i;
}
assert!(x==46);
x=1;
for(i,j) in (5..10).enumerate(){
    x+=j;
}
assert!(x==35);
イテレータに対するfor式。first iteratorを渡すだけで範囲内を横断してくれるっぽい。
let a:[u32;4]=[10,20,30,40];
for(x,y) in a.iter().enumerate(){
    // do something
}
continueループラベル。便利だが、若干goto的な香りがする。
'outer: for x in 0..10{
    'inner: for y in 0..10{
        if x%2==0{continue 'outer;}else if y%2==0{continue 'inner;}
    }
}

ムーブセマンティックス、借用 #1

プリミティブ型はcopy traitを実装しているためコピーができるという。
let v:i32=42;
let v1:i32=v;
assert!(v==v1);
しかし、vectorなどcopy traitを実装していない型(クラス?)オブジェクトに対する以下のような記述は違法だという。
let v=vec![1,2,3];
let v1=v;
assert!(v==v1); // ill-formed
何故ならば、copy traitを実装していない型にoperator =を用いると、それはムーブセマンティックスとなるからである。ここまでの範囲だけで考えると、個人的にはC++のauto_ptr的なものの二の舞に感じてしまうのだが大丈夫なのだろうか。
...まあ取り敢えず、シンタックスとセマンティックスの兼ね合いの話は一先ず置いておいて、copy traitを実装していない型のオブジェクトを、特に何も返す必要がない関数に渡す場合を考える。
fn foo(x:Vec<u32>){
 // 処理的には何か値を特に返したいわけではない関数
}

fn main(){
    let vec=vec![1,2,3];
    foo(vec);
    // vecは使えない
}
void関数として定義したいところであるが、引数はムーブされるので、渡した後に呼び出し側でその変数を使う事はできない。
ではこう書けば良いのではないだろうか。
fn foo(v:Vec<u32>,v1:Vec<u32>)->(Vec<u32>,Vec<u32>){
    // do something
    (v,v1)
}

fn main(){
    let v=vec![1,2,3];
    let v1=vec![1,2,3];

    let (v,v1)=foo(v,v1);
}
確かに、これはwell-formedである。が、これはどう考えても無駄な記述/処理である上に、void関数を定義するという意味合いとは違う。ではどうすれば良いのか。
そんな時に登場するのが借用という概念であるとのこと。
fn foo(v:&mut Vec<u32>,v1:&mut Vec<u32>){
    v.push(42);
    v1.push(42);
}

fn foo_n(v:&Vec<u32>,v1:&Vec<u32>)->u32{
    // do something
    42
}

fn main(){
    let mut v=vec![1,2,3];
    let mut v1=vec![1,2,3];

    foo(&mut v,&mut v1);
    
    assert!(v==v1);
    assert!(foo_n(&v,&v1)==42);
}
関数内で変更を加えるのであれば、まず当然だが変数宣言、次に引数への差し込み時の指定、最後に関数パラメータ指定、それぞれにmutableである事を明示しなければならない。
まあ単純に、借用という言葉は、const T&の事であって、&mut参照というのはT&の事なのだろうなと私は解釈した。つまりrustという言語は、何か*3に渡す*4という行為を行う時、copy traitが実装されていない場合、どのような形であれ、const T&か、T&、T&&のみしか受け付けない。つまり、copy traitを実装しない限りは、常にユニークな存在であるという事になる。
なるほど、「rustによる並行処理」なるワードを良く聞く事のできる要因というのは、言語仕様から成り立っているのだなと思うのだが。
余談にはなるが、この辺り、個人的にはC++で考えると捉えやすい。コピーコンストラクタと、operator=をdeleteすれば同じような状態が出来上がる。C++で大まかに同じような意味合いを示すとすればこういった感じになる。
struct X{
    X()=default;
    ~X()=default;
    X(const X&)=delete;
    X& operator=(const X&)=delete;

    struct Only_X_tag{};
};

void f(const X&){}
template<class _Tp>
auto g(_Tp&&)->decltype(typename std::remove_reference_t<_Tp>::Only_X_tag(),void()){}

void h(X){} // cannot use

int main()
{
    X x;
    f(x);
    g(x);
    g(X());
    //h(x);
    
    //X y;
    // y=x;  cannot use
}
しかし、ここまでの章に来る間でも何度か感じたが、全てデフォルトでimmutableであったり、特別にcopy traitなるものを実装しなければコピーはできないとする辺り、デフォルトの挙動がC++とは相反しているな〜と思う。その辺りは、このrustという言語の基本的な設計思想であるのだろう。

ムーブセマンティックス、借用 #2

以下のコードはill-formedである。
let mut x=21;
let y=&mut x;
*y+=21;
assert!(x==42);
何故ならば、借用には以下のような”ルール”*5があるのだという。
First, any borrow must last for a scope no greater than that of the owner. Second, you may have one or the other of these two kinds of borrows, but not both at the same time:
  • one or more references (&T) to a resource,
  • exactly one mutable reference (&mut T).
つまり、大まかにまとめると、「データ所有者のスコープよりも長い寿命でなくかつimmutableな1つ以上の参照あるいは、1つのみのmutableな参照のどちらかで、借用を用いる事が出来、前者と後者の共存は許されない」という事である。この条件に則った改修版は以下のように書かれる。
let mut x=21;
{
    let y=&mut x;
    *y+=21;
}
assert!(x==42);
単純にスコープを追加するだけだ。スコープの追加によって、yがxよりも先に死ぬ事が保証される。

ライフタイム

解放された領域へアクセスしてしまわないよう、ライフタイムという概念を用いて防止する。またスコープに明示的に名付けできる。デフォルトでは、省略された形を用いて記述している。
struct X<'a>{
    x:&'a i32,
}

impl<'a> X<'a>{
    fn ac_x(&self)->&'a i32{self.x}
}

fn main(){
    let y=&42;
    let f=X{x:y};
    assert!(f.ac_x()==&42);
}

ミュータビリティ

let mut x:u32=42;
x=10;
std::size_t x=42;
x=10;
である。
let mut x:u32=42;
let y=&mut x;
*y=10;
std::size_t x=42;
std::size_t* const y=&x;
*y=10;
である。
let mut x:i32=42;
let mut z:i32=52;
{
    let mut y=&mut x;
    *y=10;
    y=& mut z;
    *y=10;
}
std::size_t x=42,z=52;
{
    std::size_t* y=&x;
    *y=10;
    y=&z;
    *y=10;
}
である。*6
また以下のコードはill-formedである。
struct Point{
    x:i32,
    mut y:i32, // ill-formed
}
cloneのような参照カウントを変更する場合において、ユーザー側のミュータブル指定以外での、フィールドレベルミュータビリティが欲しい。しかしrustは、言語レベルでフィールドのミュータビリティに対応していない。そのため、Cellを用いてエミュレートする形となる。
use std::cell::Cell;

struct Point {
    x: i32,
    y: Cell<i32>,
}

let a=Point{x: 5,y:Cell::new(6)};
a.y.set(7);
上記のような条件がなく、単純なミュータビリティを求めるのであれば、当然だがユーザー側でミュータブル指定を行う事で実現できる。
struct Point{
    x:i32,
    y:i32,
}

fn main(){
    let mut a=Point{x:0,y:0}; // mutable束縛
    a.x=5;
    let a=a; // 新しいimmutable束縛
    a.x=6; // エラー
}

構造体

アップデート構文というものがある。これは、構造体同士の一部を別のオブジェクトメンバに依存して新規生成したい場合に用いる。
struct Point3d{
    x:i32,
    y:i32,
    z:i32,
}

fn main(){
    let a=Point3d{x:0,y:0,z:0};
    let b=Point3d{x:42,y:42,..ptr};

    assert!(a.z==b.z);
}

タプル/unit-like構造体

タプルと構造体のハイブリットのようなデータ型があるという。構造体自体には名前付きであるが、フィールドには名前が付かない。
実際にこの機能が役立つときは、1要素で使う場合のみだという。1要素のみでタプル構造体を用いた場合、セマンティック的に明確な表現となる、新しい型を作成でき事から、newtypeパターンと呼ばれる。
struct SignedIntegerType32(i32);

fn main(){
    let int=SignedIntegerType32(42);
    let SignedIntegerType32(value)=int;
    assert!(value==42);
}
上記のように束縛する事で実際の値を参照する事ができる。
また、全くメンバを持たない構造体を定義する事ができ、それはunit-like構造体と呼ばれる。
Struct SomethingTag;
let x=SomethingTag;
利用用途としては、ライブラリがイベント処理できる特定のtraitが実装された構造体の作成を要求する場合やライブラリ開発の際のタグ付けなどが考えられる。

列挙型

enum X{
    a,
    b(i32,i32,i32),
    c{x:i32,y:i32},
    d(String),
}

fn main(){
    let a:X=X::a;
    let b:X=X::b(0,1,2);
    let c:X=X::c{x:21,y:42};
    let d:X=X::d("hoge".to_string());
}
列挙型を以下のように取り出す事はできない。
let X::b(x,y,z)=b;
後述するが、利用するにはoperator=をユーザーが実装する方法とmatch式で変数のパターンマッチを行う方法がある。

match式とパターン

C++で言えば、switch文に該当するような機能だろうか。
しかしmatch式は網羅性をとても重要視している。例えば、以下のような式はill-formedである。
let x = 5;
match x {
    1 => println!("one"),
    2 => println!("two"),
    3 => println!("three"),
    4 => println!("four"),
    5 => println!("five"),
}
公式ドキュメントには以下のように書かれている。*7
One of the many advantages of match is it enforces ‘exhaustiveness checking’. For example if we remove the last arm with the underscore _, the compiler will give us an error:
error: non-exhaustive patterns: `_` not covered
Rust is telling us that we forgot some value. The compiler infers from x that it can have any 32bit integer value; for example -2,147,483,648 to 2,147,483,647. The _ acts as a 'catch-all', and will catch all possible values that aren't specified in an arm of match. As you can see in the previous example, we provide match arms for integers 1-5, if x is 6 or any other value, then it is caught by _.
xが整数型であるので様々な整数値がxに入る事がコンパイラは想定できる。それに対する全ての選択肢に対応できなければコンパイルエラーとなるのである。つまり、以下のような式を追加しなければならない。
_ => "something else",
複式パターンを記述する事もできる。
let x = 1;

match x {
    1 | 2 => println!("one or two"),
    3 => println!("three"),
    _ => println!("anything"),
}
構造体やタプル、列挙型をパターン内で分解する事ができる。
struct X{
    x: i32,y: i32,
}

let a=X{x:0,y:0};
match a{
    X{x,y}=>println!("({},{})", x, y),
}
引数に別名を付与したい場合は以下のように記述できる。
match a{
    X{x:x1,y:y1}=>println!("({},{})", x1, y1),
}
値の一部のみを使いたければ、以下のように記述できる。
match a{
    X{x,..}=>println!("({})", x),
}
引数の順序に対して無関係に処理が可能である。
match a{
    X{y,..}=>println!("({})", y),
}
これら一連の振る舞いは、デストラクチャリングと言われる。
また束縛を無視する事もできる。
enum tuple{
    Values(i32,i32,i32),
    Missing,
}

fn main(){
    let x=tuple::Values(5,-2,3);
    match x{
        tuple::Values(..)=>println!("tuple"),
        tuple::Missing=>println!("no such luck"),
    }
}
リファレンスを取得したい場合は以下のように記述できる。
let x:i32=42;
let mut y:i32=21;
match x{ref r=>println!("{}",r),}
match y{ref mut mr=>println!("{}",mr),}
...で、rangeのマッチを行う事ができる。
let x='n';
match x{
    'a'...'j'=>println!("a through j"),
    'k'...'z'=>println!("k through z"),
    _=>println!("something else"),
}
rangeに対して一定の名前を結びつけたい場合は以下のように記述できる。
let x='n';
match x{
    a @ 'a'...'j'=>println!("{}",a),
    b @ 'k'...'z'=>println!("{}",b),
    _=>println!("something else"),
}
ifを用いる事でマッチガードを実現する事ができる。
let x=4;
let y=false;

match x {
    4 | 5 if y => println!("yes"),
    _ => println!("no"),
}
if式は4と5の両方を修飾するためnoを出力する。
それでは先ほど述べたmatch式で、列挙型を取り出してみよう。
enum X{
    a,
    b(i32,i32,i32),
    c{x:i32,y:i32},
    d(String),
}

fn main(){
    let a:X=X::a;
    let b:X=X::b(0,1,2);
    let c:X=X::c{x:21,y:42};
    let d:X=X::d("hoge".to_string());

    get_enum(a);
    get_enum(b);
    get_enum(c);
    get_enum(d);
}

fn a_(){ /* do something */}

fn get_enum(x: X){
    match x{
        X::a=>a_(),
        X::b(a,b,c)=>println!("{},{},{}",a,b,c),
        X::c{x: x,y: y}=>println!("{},{}",x,y),
        X::d(s)=>println!("{}",s),
    };
}
match式とパターンによる分岐はとても強力に感じる。ライブラリで実現させるような機能が、言語機能として一般化されている。
続く。

参照

*1:バイナリ製作において

*2:文から返すものはなにもない

*3:変数、パラメータ等

*4:参照、ムーブ等の意味

*5:References and Borrowing - The Rules

*6:厳密にはstd::uint32_t

*7:Match一部引用