概要
最近Rustの話をよく聞くようになったので、どんなものか触ってみた。
Rust勉強してみたー前半戦の続き。
初心者にとって、前半戦は天国で後半戦は地獄。
はじめに
##Rustバージョン
Rustのバージョンは変わらずrustc 1.10.0 (cfcb716cf 2016-07-03)
です。
##開発環境
勉強の流れで必要になった+便利な機能にあやかりたくなったので、cargoを導入しました。
といっても、公式インストーラでRust入れた場合は勝手に入っているとのことで、
確認してみたら確かに入っていました。
バージョンはcargo 0.11.0-nightly (259324c 2016-05-20)
です。
それとcargo-editも導入してみました。上級者っぽく見えるかなって思って。
この記事を参考にしました。
##参考サイト
以下の通り。その他、前記事コメント欄で教えていただいたサイトもちょくちょく見てました。
プログラミング言語Rust
Rust Documentation
AtCoder(競プロサイト。日本語且つRustに対応しているので、とても良い。)
##その他
後半戦はプログラミング言語Rustの4.16~4.36をやりました。
応用的な構文を勉強した感じ。
注意:実装しているときに引っかかったことだけ書くので、言語の強みとか記述方法とかは参考サイトを見てください。
引っかかり点その1-trait
8/05追記
コメントでご指摘いただきましたので、サンプルコードのtrait名などを『Debug』から『Test』に変更致しました。
理由は、標準ライブラリに同名traitが存在しており、競合を起こす可能性があったためです。
同名traitは極力避けるか、別名moduleで括るなどの処理を施しましょう。
Rustでは、任意の型が提供する関数群、としてtraitという機能を提供している。
C++的感覚としては、インターフェース、基底クラス的なもの?
ちょっと違うとは思うけど、そう考えるとしっくりくる部分もある。
以下のように記述する。
struct A;
trait Test
{
fn TS_Print( self );
}
impl Test for A
{
fn TS_Print( self: A ){ println!( "test A!" ); }
}
fn main()
{
let a: A = A{};
a.TS_Print();
}
そして、このtraitを使って動的ディスパッチを実現しているのが、trait objectsという機能。
コンパイル時ではなく実行時に、型の判定やら呼び出す関数の決定やら諸々やる。
メリット :該当する型の分だけコードを量産する、とかはしないのでコード量は増えない
デメリット:inlineができない、実行時処理が増える
C++的感覚としては、子クラスを親クラスのポインタに突っ込んで仮想関数やらなんやら使う時の感じ?
そう考えると、こういう書き方はせずに、引数に指定しているtraitに関数追加すれば?って感じもするけども。
以下のような感じで書く。
struct A;
trait Test
{
fn TS_Print( &self );
}
impl Test for A
{
fn TS_Print( self: &A ){ println!( "test A!" ); }
}
fn Print( a: &Test )
{
a.TS_Print();
}
fn main()
{
let a: A = A{};
Print( &a as &Test );
}
動的ディスパッチがあるなら静的ディスパッチもある、ということで、
ジェネリックと組み合わせると、コンパイル時に諸々決定してくれる。
メリット :実行時処理が減る、inlineできる
デメリット:該当する型の分だけ同じコードを量産するので、コード量が膨大に。
コード量によってはキャッシュに乗らなくて読み替えが走ったりするのかもしれない。
C++的感覚としては、まんまテンプレートって感じ。ただし、trait boundsという機能のおかげで、
該当する型への制限がかけやすい。C++だと『明示的なインスタンス化』くらいしか思いつかないけど、
それと比べると、とても便利。
以下のような感じで書く。
struct A;
trait Test
{
fn TS_Print( &self );
}
impl Test for A
{
fn TS_Print( self: &A ){ println!( "test A!" ); }
}
fn Print<T>( a: &T ) where T: Test
{
a.TS_Print();
}
fn main()
{
let a: A = A{};
Print( &a );
}
ちなみにwhere T: Test
の所は、where i32: Test
みたいに具体的な型も指定できる。
で、この辺の基本的な記述内容は別に問題なかったんだけど、すごくどうでもいい所で引っかかった。
trait Test
{
fn TS_Print( &self );
}
struct A<T> where T: Copy
{
a: T,
}
impl<T> Test for A<T> where T: Copy + Clone
{
fn TS_Print( self: &A<T> ){ println!( "test A!" ); }
}
fn main()
{
let a: A<i32> = A{ a: 10 };
a.TS_Print();
}
where文の所なんだけど、構造体宣言箇所と処理実装箇所で書く内容が散らばるの辛くない?
試してみた感じ、処理実装箇所は構造体宣言箇所の指定に依存するから、
片方だけ修正しちゃった時とかに、必ずコンパイラに怒られるんだよなぁ。
まぁ怒られるからいいのかなぁ…
引っかかり点その2-closure
他の言語だと、無名関数とかラムダ式とか言われるもの…だと思う。
関数とは違って、引数指定してなくても、スコープ外の変数にアクセスできたりする。
比較用のコールバックとかでいちいち関数化するのがめんどい時とかに使う?あんまり便利だと思っていない。
以下のような感じで書く。
fn main()
{
let mut num = 10;
{
let mut add = | x: i32 |{ num += x };
add(10);
}
println!( "num = {}", num );
}
num = 20
引っかかり点としては、以下の辺りだろうか。
・スコープ外の変数にアクセスする場合は、closureを格納する変数にmut
を付ける必要がある。
・借用になる場合とmoveになる場合(非明示時)がわかりづらい。
スコープ外の変数単体しか記述されていない時だけmoveする?
・Fn trait関連
Fn trait関連だけ詳しく書く。
closureの構文||{}
は内部的にFn<Args>
、FnMut<Args>
、FnOnce<Args>
を実装している。
これは()
演算子が実装しているものと同じなので、実質関数ということになる。
つまり関数とclosureの違いは、独自のスタックフレームの有無とかはあるけど、特にないと考えてよくて、
それ故に、以下のような使い方ができるとのこと。
fn ExecFunc( x: &Fn( i32, i32 ) -> i32 ){ println!( " {} ", x( 10, 5 ) ); }
fn AddFunc( x: i32, y: i32 ) -> i32 { x + y }
fn main()
{
let SubFunc = | x: i32, y: i32 | -> i32 { x - y };
ExecFunc( &SubFunc as &Fn( i32, i32 ) -> i32 );
ExecFunc( &AddFunc as &Fn( i32, i32 ) -> i32 );
}
しかしながら以下のような書き方はできない。
fn ExecFunc( x: fn( i32, i32 ) -> i32 ){ println!( " {} ", x( 10, 5 ) ); }
fn AddFunc( x: i32, y: i32 ) -> i32 { x + y }
fn main()
{
let SubFunc = | x: i32, y: i32 | -> i32 { x - y };
// ExecFunc( SubFunc ); //NG.closureは関数ポインタとしては渡せない.
// ExecFunc( AddFunc ); //OK.普通の関数ポインタ.
}
Fn(i32, i32) -> i32
はtraitで、これは関数にも実装されているので、
単純にtrait objectsとして適用可能。
しかし、fn(i32, i32) -> i32
は関数ポインタ用の構文だからclosureには適用不可。
ということですかね…こう考えるとしっくりきますね…
引っかかり点その3-crate
他の言語でいう、ライブラリ的なもの。
説明はここを参照してください。
引っかかった所は、crate内の各moduleに共通の関数を持たせようとtraitを実装した時に、
pub
を指定する箇所でコンパイラに怒られまくった所。
以下のように、traitブロックの先頭にだけ書くのが、正解のようです。
関数の先頭とかめっちゃ試したわ…
pub trait DebugFunc
{
fn Print( self: &Self );
}
impl DebugFunc for A
{
fn Print( self: &A ){ println!( "A" ); }
}
impl DebugFunc for B
{
fn Print( self: &B ){ println!( "B" ); }
}
あとcargo-editでcrateを追加する時に、
--optional
を付けたらコンパイル通らなかった。そっと消した。
ローカルで適当に作ったcrateだったからだと思う。
引っかかり点その4-関連型
参考サイト曰く、『複数の型をグループ化するもの』とのことだが、よくわからず。
C++の似ている概念も思いつかず、完全に新規な概念なので、よくわからず。
traitブロック内に型宣言を含むことができて、implブロック内で具体的な型を指定できる。
演算子のオーバーロードでもこれが使われている模様。
以下のような感じで書く。
struct A;
struct B;
trait C
{
type TMP;
fn Add( self: Self, x: Self::TMP, y: Self::TMP ) -> Self::TMP;
}
impl C for A
{
type TMP = i32;
fn Add( self: A, x: i32, y: i32 ) -> i32 { x + y }
}
impl C for B
{
type TMP = String;
fn Add( self: B, x: String, y: String ) -> String { x + &y }
}
fn main()
{
let a: A = A{};
let b: B = B{};
println!( "add = {}", a.Add( 10, 15 ) );
println!( "add = {}", b.Add( "hello,".to_string(), "world.".to_string() ) );
}
使いどころわからんなぁ、って思ってたけど、
サンプルコード書いてたら、めっちゃ便利じゃない!?!?!?!ってなった。
これのおかげで、traitは処理も型もすべて抽象化できているわけですね。
型のみ宣言してあるtraitとかもどこかで使い道があるのかもしれない。
総括
とりあえず基本的な所は一通り勉強し終わりました。
後半戦は前半戦と違って、特殊で上級な感じがする構文が多かったです。
幸いC++をよくいじっているので、genericやらtrait周りは特に引っかからずいけましたが、
macro辺りは良い感じに辛い感じでした。多分書けるようにならないと思う。BNF記法とか苦手なのでより辛い。
最初は、所有権とかでガチガチに縛られてるなぁ、という印象だったんですが、
一通り勉強してみると、意外と自由度は高いんだなぁ、という印象に変わりました。
締める所は締める、緩い所は何やっても良し、という感じは、理想的な会社っぽいなって思いました。
とりあえず今後はEffective Rust辺りを攻めていこうと思います。
どうでもよいこと
いろいろモジュールを組み合わせ始めると、所有権回りが辛そうだなぁって思った。
外部モジュールを使おうにも、中身見てどういった参照方法を取っているかとかを確認しないと、
コンパイラに怒られそう。そもそもライブラリにするような関数で値をいじるのがおかしいのかしら。
あ、本文で間違っている所があったら、随時修正しますので教えてください。よろしくお願いいたします。