Rust言語の構造体と列挙体がすごく便利で面白い
Rust言語を実務で使うため、しばらくLinux Mintで構築した環境でいろんなcrateを試していた。
で、大体のcrateで出てくるのが、『構造体』と『列挙体』でのmatchと値の取り出し。
『構造体』は他言語で言うところのClassみたいに扱える定義。
『列挙体』についてはenumと呼ばれており、あるデータが取りうる状態を登録しておく定義。
概念的にはこんな感じ。
C言語などのコンパイル型言語ではメジャーな話であるだろうに、前者はともかく後者はスクリプト型言語が主流であるWeb系の界隈ではほとんど聞かない。
コンパイル型言語でWeb APIなどをガシガシ作っている方々はともかくとして、一般的なWebエンジニアに「列挙体ってなぁに?」って聞いても「え?なにそれ?」と質問を質問で返すことだろう。
恥ずかしながら自分もRust言語(と、それに伴って半ば強制的にコンバートしなければならないC言語の熟読)をやるまで、列挙体というものに触れた機会なんかほぼない。
スクリプト型言語では列挙体なんてまず出てこないし、ましてや大雑把にでも紐解くなんてやろうと思わなかっただろう。
というか、そもそも無いものはやる必要がない。
自分にとっての未知には手を出さず、クサいものにはフタ。Web系の誰もが例外なく持ってる暗黙のルール。
と言いつつもコンパイル型言語に手を出して結構経っている以上、今回は構造体と列挙体について紐解いて、理解に変えていくというお話になる。
これらを使って、単にデータを引数として取り込んで戻り値にするという処理を作成してみる。
事の発端としては、SDL2やactix-webを入れての各crateのチュートリアルの際。
頻繁に以下のようなコードの体系が出てきたもんだから、これってなんぞやとずっと思っていた。
// Rust
let map = Map{ format: Position::Point( 0, 0 ) };
let map_context = map.init();
match map_context {
Pin::Needle { .. } => {
// なんか処理
},
}
正しく未知の記述方法。
まぁ書けば動くから、作法的にそれでいいんだなーとか思っていたけど、基本的なチュートリアルをやり終えて、とりあえずサンプルでなんか作ってみっかと思ったときに問題に気が付いた。
そう、そもそもチュートリアルなんて、基本的にせいぜいmainに対してコードをざらざらと書いていくものである。
すなわち、チュートリアル時点では処理のグルーピング化だとか、更にそれのモジュール化だなんてものは一切考慮しない。
これはそれなりにきっちりしたサンプルのプロジェクトを作るときには結構な問題となる。
何故ならば、計算を伴わないデータの読み出しだけならそれだけで処理をグルーピングしておきたいし、データ整形だけならそれはそれで処理をグルーピング化しておきたい。
他、計算処理だけを行いたい場合とか、画面へのスプライトだけを行いたい・・・これらを一括してパッケージ化するべくモジュール化したい。
・・・得てして、そういった欲が出てくるからだ。
また、大きな問題としてこれが致命的になるのが、複数の型と複数の値を含んだ一連のデータが出てくるとき。
例えば、CSVやDBテーブルなどのレコード一行をただ取り出した単純なデータで、『id』『name』『value』『sale』と言った4列に分かれているものがあったとする。
これはPHPやRubyとかを例にすると、以下のような配列(ハッシュ)で実現できるが・・・
// PHP
$record = array( 'id' => 1, 'name' => 'cup', value => 200, 'sale' => true );
// Ruby
record = { "id" => 1, "name" => "cup", value => 200, "sale" => true }
これらであれば、半ばこれをそっくりそのまま構造として使うことができるわけである。
ここで注意したいのは、これらの言語は全て動的型付言語だということだ。
これはスクリプト型言語の良いところと同時に悪いところでもあり、静的型付が主のコンパイル型言語でスクリプト型言語の動的型付の思考を持ち出すと転じてこれが最低最悪レベルで牙を剥く。
ぶっちゃけいうと、Rust言語では配列を使うにもきちんと型宣言が必要だし、そもそも同じ配列内では異なる型を含むことは一切できない。
えっ、HashMap使えって?PHPとかRubyとかと同じような感覚で構造化できないでしょ、あれ。
スクリプト型言語がどれだけガバくてユルいゆとり言語なのかは、普段からそれだけ使っていた俺にはあまり自覚がない。
その昔に「スクリプト型言語はプログラミング言語ではない」という言葉が、どこかしこのコミュニティに散見されたけど、今更ながらそれをより強く感じる。
当時は「いやいや、プログラミング言語でしょwww」とか笑っていたが・・・。
申し訳ありません・・・!!
やっぱりスクリプト型言語はプログラミング言語ではありませんでしたぁぁぁ・・・!!!
先人のお言葉は、コンパイル型言語を主として使うようになった今になって、壮絶なクリティカルストライクを決めてくれた。
・・・トホホ。
何にしても、Rust言語でこれらを行うときに必ず避けて通れなくなるのが、件の構造体と列挙体になる。
一言に構造体や列挙体と言っても用途は色々で、ごくごく基本的な記述だけ載せると、
// 単純な構造体
struct Item{
id: i16,
name: String,
value: i32,
sale: bool,
}
// 処理を伴う構造体
struct Exec {
no: i32,
data: String,
}
impl Exec {
fn get( &self ) -> String {
// 処理
}
}
// 構造体のインスタンス生成・実行
let exec = Exec{ id: 0, data: "" };
exec.get();
// 単純な列挙体
enum Customer {
Id,
Name,
Address,
}
// 構造体やタプルを含んだ列挙体
enum Formats {
Title( String ),
StrItem{ id: i16, name: String, value: i32, sale: bool },
StrCustomer{ id: i16, name: String, address: String },
TpItem( i16, String, i32, bool ),
TpCustomer( i16, String, String ),
}
// 列挙体のインスタンス生成
let title = Formats::Title;
let item = Formats::StrItem{ id: 0, name: "", value: 0, sale: false };
こんな感じで構造化もしくは列挙できる。
structで宣言した構造体は処理役割だけではなくキーを持った配列的構造もあらかじめ定義できる・・・この辺は他言語のClassも同じ。
ちなみに、処理を伴う構造体についてはClassのようにtraitすることもでき、オブジェクト指向が好きな人にも親和性が多少ある。
・・・まぁRust言語自体にオブジェクト指向的な考え方は薄いし、使用感は既知のtraitとは似て非なるものだけど。
そして、enumで宣言した列挙体は構造体形式やタプル形式で様々なものをテンプレート的に定義できる。
タプル形式だけじゃなくて、キー付きの構造体まで定義できるのは本当にありがたい・・・!
利点は言わずもがなプログラムの最初の段階で使いたい設計や構造を明示できるし、これはコンパイル型の明らかな優位点で、スクリプト型言語のように定義行の多さがそのまま動作に大打撃を与えることもない。
そして、最大の特長として、構造体も列挙体も変数に型として以下のように宣言できること。
型が厳格に管理されているRust言語においては、変数ごとのリテラルや型の明記はなるべく画一化させたいので、細かいところが気になる人には好都合。
let item_data: Item = ~~~; let customer_data: Customer = ~~~;
要は自らユーザ定義の擬似的な型を作り出す事ができるわけだ。
crateで構造体が使われている場合にも有効で、内部で使用されている構造体の名前などをいちいち調べる手間はあるが、何が何でも型を書いておきたいという人には良いかも。
また、勘の良い方はこの時点でお気付きかもしれないが、これは戻り値の型としても使用できる。
impl Exec {
fn get( &self ) -> Item {
// 処理
}
}
このあたりは、厳格な静的型付言語であるRust言語らしいと言えばRust言語らしい書き方だね。
まさに配列様式的な内容ひとつをとってもこのように宣言しなければならないと言うことが、もどかしくて面倒くさいと感じるか清々しくて分かりやすいと感じるかは人それぞれだが、静的型付の言語とは元来こういうものだ。
(ちなみに自分は後者。いくらPHPだとは言え変数の管理はしたいので必ず様式の宣言的なことは書いてる。)
理解が進んだところで、これを踏まえて実験コードを書いて遊んでみた。
冒頭で書いていた『単にデータを引数として取り込んでそのまま戻り値にしてやるという処理』は以下のような形で書くことができる。
//
// 1. タプル的enumから構造体enumに取込→通常変数に返し直す実験
//
// 入りデータの列挙フォーマット(タプル)
enum RecordFormats {
Items( i16, String, i32, bool ),
Customers( i16, String, String ),
}
// 戻りデータの列挙フォーマット(構造体)
//#[derive(Debug)]
enum DataFormats {
Items{ id: i16, name: String, value: i32, sale: bool },
Customers{ id: i16, name: String, address: String },
}
// インプリメントの構造体
struct Recorder {
record: RecordFormats,
}
// インプリメントの処理
impl Recorder {
// 入りデータを戻りデータに変換して単純に返すメソッド
fn get( &self ) -> DataFormats{ // 戻り値の型はenum DataFormatsを型として参照できる
// 入りデータの構造体をマッチしてデータを取り出し
match &self.record { // 自身の構造体のパラメータがマッチ条件(&selfでの参照取り出しのため、以後の数値等のアクセスはポインタになる)
RecordFormats::Items( id, name, value, sale ) => { // Items各値のインスタンス化
// 取り出した入りデータを戻りデータ様式に整形して返還
let data: DataFormats = DataFormats::Items{
id: *id,
name: name.to_string(),
value: *value,
sale: *sale,
};
return data;
},
RecordFormats::Customers( id, name, address ) => { // Customers各値のインスタンス化
// 取り出した入りデータを戻りデータ様式に整形して返還
let data: DataFormats = DataFormats::Customers{
id: *id,
name: name.to_string(),
address: address.to_string(),
};
return data;
},
}
}
}
// メイン処理
fn main() {
// Items様式を取り込み
let mut record = Recorder{
record: RecordFormats::Items( 1, "cup".to_string(), 200, true ),
//record: RecordFormats::Items( 2, "spoon".to_string(), 100, true ),
};
let mut record_data = record.get();
//println!( "{:?}", record_data ); // メッセージを出す場合、enum DataFormatsに#[derive(Debug)]が必要
match record_data {
DataFormats::Items{ id: id, name: name, value: value, sale: sale } => { // 格納先変数名: 列挙体のデータ名で値受け取り
println!( "{} {} {} {}", id, name, value, sale );
},
DataFormats::Customers{ id: id, name: name, address: address } => { // 格納先変数名: 列挙体のデータ名で値受け取り
println!( "{} {} {}", id, name, address );
},
}
// Customers様式を取り込み
record = Recorder{
record: RecordFormats::Customers( 1, "john".to_string(), "unknown".to_string() ),
//record: RecordFormats::Customers( 1, "jane".to_string(), "where".to_string() ),
};
let mut record_data = record.get();
match record_data {
DataFormats::Items{ id: id, name: name, value: value, sale: sale } => { // 格納先変数名: 列挙体のデータ名で値受け取り
println!( "{} {} {} {}", id, name, value, sale );
},
DataFormats::Customers{ id: id, name: name, address: address } => { // 格納先変数名: 列挙体のデータ名で値受け取り
println!( "{} {} {}", id, name, address );
},
}
// 結果
// 1 cup 200 true
// 1 john unknown
}
//
// 2. 構造体enumからタプル的enumに取込→通常変数に返し直す実験
//
// 入りデータの列挙フォーマット(構造体)
enum RecordFormats {
Items{ id: i16, name: String, value: i32, sale: bool },
Customers{ id: i16, name: String, address: String },
}
// 戻りデータの列挙フォーマット(タプル)
//#[derive(Debug)]
enum DataFormats {
Items( i16, String, i32, bool ),
Customers( i16, String, String ),
}
// インプリメントの構造体
struct Recorder {
record: RecordFormats,
}
// インプリメントの処理
impl Recorder {
// 入りデータを戻りデータに変換して単純に返すメソッド
fn get( &self ) -> DataFormats { // 戻り値の型はenum DataFormatsを型として参照できる
// 入りデータの構造体をマッチしてデータを取り出し
match &self.record { // 自身の構造体のパラメータがマッチ条件(&selfでの参照取り出しのため、以後の数値等のアクセスはポインタになる)
RecordFormats::Items{ id, name, value, sale } => { // Items各値のインスタンス化
// 取り出した入りデータを戻りデータ様式に整形して返還
let data: DataFormats = DataFormats::Items( *id, name.to_string(), *value, *sale );
return data;
},
RecordFormats::Customers{ id, name, address } => { // Customers各値のインスタンス化
// 取り出した入りデータを戻りデータ様式に整形して返還
let data: DataFormats = DataFormats::Customers( *id, name.to_string(), address.to_string() );
return data;
},
}
}
}
// メイン処理
fn main() {
// Items様式を取り込み
let mut record = Recorder{
record: RecordFormats::Items{ id: 1, name: "cup".to_string(), value: 200, sale: true },
//record: RecordFormats::Items{ id: 2, name: "spoon".to_string(), value: 100, sale: false },
};
let mut record_data = record.get();
//println!( "{:?}", record_data ); // メッセージを出す場合、enum DataFormatsに#[derive(Debug)]が必要
match record_data {
DataFormats::Items( id, name, value, sale ) => { // タプル的に値参照
println!( "{} {} {} {}", id, name, value, sale );
},
DataFormats::Customers( id, name, address ) => { // タプル的に値参照
println!( "{} {} {}", id, name, address );
},
}
// Customers様式を取り込み
record = Recorder{
record: RecordFormats::Customers{ id: 1, name: "john".to_string(), address: "unknown".to_string() },
//record: RecordFormats::Customers{ id: 2, name: "jane".to_string(), address: "where".to_string() },
};
record_data = record.get();
match record_data {
DataFormats::Items( id, name, value, sale ) => { // タプル的に値参照
println!( "{} {} {} {}", id, name, value, sale );
},
DataFormats::Customers( id, name, address ) => { // タプル的に値参照
println!( "{} {} {}", id, name, address );
},
}
// 結果
// 1 cup 200 true
// 1 john unknown
}
これ自体の処理には何の意味もなく、コピペとしては全く使えないコードである。
(プログラムの開始段階で設計が既に確定しているコンパイル型言語に、プログラムを開始した後で設計を行っていくスクリプト型言語のようなコピペコードが存在できるかどうかははてさて置いといて。)
ただし、内容を理解すると言った点では非常に有益。
『引数としての入りデータ』と『戻り値としての戻りデータ』とで、それぞれの構造体や列挙体で示したフォーマットが違うことがミソ。
これならインプリメント内のメソッドで、データを整形したり、行列データとしてVecで返したり、データから計算or計上した結果を返したり・・・といった芸当も可能。
紐解いていくまでは、単なるClass的なものだと思っていた構造体も、イマイチ使い所がおぼろげだった列挙体もこれで明確に使えるようになった。
これ、すげぇ便利で面白いやん!
学習コストが莫大と言われるRust言語だけど、特性を少しずつ掴んでいけば本当に実直な言語よね~。
まぁ、そもそもが宇宙語であると言うことには変わりないけどさ。
以上、久しぶりの投稿すぎて年が明けてからしばらく経って、あけおめも言えない時期になってしまったことを悔やむ記事です。毎度のことです、はい(´・ω・`)
それでは、明けましておめでとうございます!!(もう遅いわ!!!!)
