Rustプログラミング言語の8章練習問題をやってみた
TL;DR
プログラミング言語Rustの8章練習問題をやってみました。Rust初心者なので回答内容は保証しませんが、参考まで。
プログラミングはやっぱり面白い
退職して2年近く。仕事としてプログラミングを書かなくなり、やる気もなくなってました。しかし、改めて遊びでもプログラミングしてると時が経つのを忘れます。在職中からやってるスマホゲームとか、あるいはパソコンのゲームとかもいくつかやってはいますが、なんとなくやる気が出ないで詰み気味。そんな中でも、練習問題とは言えプログラミングしてると面白くなってしまい、ますますゲームとかやらなくなってしまいました。ただ、積んである本が減らないのは困りどころ。
まあ、誰もがそうではないと言うのは当然わかってます。が、プログラミングも面白いと言うことをわかって欲しいなとは感じています。
第8章
第8章ともなると、Rustの肝たる所有権システムをはじめ、本格的なプログラミング要素がかなり登場。中身も難しいというか、それなりに手間がかかるものが増えてきます。ソースも長くなってくるので、少しは解説的なことが必要でしょうか。
練習1.整数のリストから平均値、中央値、 最頻値を求める
ちょっと冗長な気は自分でもしています。もうちょっと上手く書けそう。
// 整数のリストからベクタを使ってmean,median,modeを求める
use std::collections::HashMap;
// 平均値を求める
fn calc_mean(list: &Vec<i32>) -> f64 {
let mut sum = 0;
for i in list {
sum += i;
}
let sum = sum as f64;
sum / list.len() as f64
}
// 中央値を求める
fn calc_median(list: &Vec<i32>) -> f64 {
let mut sorted = list.to_vec();
sorted.sort();
let mut median = 0.0;
match sorted.len() % 2 {
0 => {
let median1: i32 = sorted[sorted.len() / 2 - 1];
let median2: i32 = sorted[sorted.len() / 2];
median += median1 as f64;
median += median2 as f64;
median /= 2.0;
},
1 => {
let median1: i32 = sorted[sorted.len() / 2];
median = median1 as f64;
},
_ => {},
};
median
}
// 最頻値を求める
fn calc_mode(list: &Vec<i32>) -> Vec<i32> {
let mut map = HashMap::new();
let mut mode: Vec<i32> = Vec::new();
for i in list {
let count = map.entry(*i).or_insert(0);
*count += 1;
}
if let Some(max) = map.values().max() {
for (key, value) in &map {
if max == value {
mode.push(*key);
}
}
}
mode.sort();
return mode;
}
fn main() {
// 整数のリスト1(mean=50.75,median=45.5,mode=10)
let list1: Vec<i32> = vec![10, 34, 55, 76, 88, 10, 36, 97];
// 整数のリスト2(mean=29,median=10,mode=10,55)
let list2: Vec<i32> = vec![10, 8, 55, 55, 10, 10, 55];
// 整数リスト1の計算
println!("list1-> mean:{}, median:{}, mode:{:?}",
calc_mean(&list1), calc_median(&list1), calc_mode(&list1));
// 整数リスト2の計算
println!("list2-> mean:{}, median:{}, mode:{:?}",
calc_mean(&list2), calc_median(&list2), calc_mode(&list2));
}
平均についてはまあこんなものかと思います。多分、foldとか使うのが良いのでしょうか。
fn calc_mean(list: &Vec<i32>) -> f64 {
let sum = list.iter().fold(0, |acc, x| acc + x);
let sum = sum as f64;
sum / list.len() as f64
}
中央値は特に冗長感が強いです。なんか上手く書けそうなのですが。特に要素数が偶数の時の処理。1行にするとキャストがうまくできなかったのでなんかおかしな書き方になってます。これは絶対におかしいなと思ってます。
最後に最頻値。42〜43行目は他のスクリプト系言語なら定義されてないキーのハッシュは自動的に確保してくれそうなのでちょっと面倒さを感じてしまいます。
45行目はunwrap()を使う方が普通そうな気はします。ただ、個人的にはGo言語の似たような構文の影響からかif letを使ってます。
練習2.文字列をピッグ・ラテンに変換する
海外ではおなじみの一種の言葉遊びだとか。ザギンでシースーみたいなもん?
実際にはhonorみたいな黙字で始まる場合はこれも母音扱いになるとか、そもそも母音はaiueoだけで良いのだっけ?とか疑問はいろいろ。
// 文字列をピッグ・ラテン(なんちゃってラテン語)に変換する
fn convert_pig_latin(src: &str) -> String {
let words: Vec<&str> = src.trim_end_matches('.').split(' ').collect();
let mut converted: Vec<String> = Vec::new();
for word in &words {
let first_char = &word[0..1];
let others = &word[1..];
match "aiueo".find(first_char) {
Some(_) => {
// 母音で始まる場合
converted.push(format!("{}{}hay",first_char, others)
.to_lowercase());
},
None => {
// 子音で始まる場合
converted.push(format!("{}{}{}", others, first_char, "ay")
.to_lowercase());
},
}
}
let converted = converted.join(" ");
format!("{}{}.", &converted[0..1].to_uppercase(), &converted[1..])
}
fn main() {
let source_string = "This is a pen.";
let converted = convert_pig_latin(source_string);
println!("{}", converted);
}
問題の説明に「UTF-8エンコードに関する詳細を心に留めて」とあるので、ちゃんとエンコーディングとか考えないといけないのかもですが、そもそも元の文字列には英語しか入らないよね?ということで特に考慮しないで書いてます。それじゃ、練習にならないかな?
先頭が母音か子音かで処理を分ける部分が8〜20行目。そのまんまロジックを組むと「1文字目がaだったら、iだったら、…」とif文を書き連ねるかmatch文を使うことになるかと思います。が、個人的にはこの手の処理は逆転の発想で”aiueo”の中に1文字目があるか?という処理に置き換えるのを常套手段にしてます。match文ではなくてif let 〜 else のほうがよかったかも。
練習3.ハッシュマップとベクタを使って従業員リストの作成と表示
だいぶ本格的なプログラムっぽくなってきました。本当は構造体とか使うべき?
// 部署別従業員リストの追加IFと一覧表示
use std::collections::HashMap;
use std::collections::hash_map::Keys;
// 従業員リストへの追加
fn add_employee(list: &mut HashMap<String, Vec<String>>, order: &str) -> () {
// 命令文を分解
let order: Vec<&str> = order.split(' ').collect();
// 1番目と3番目の単語が"Add"と"to"であるか確認(違う場合は無視)
if order[0] != "Add" || order[2] != "to" {
return;
}
list.entry(order[3].to_string())
.or_insert(Vec::new()).push(order[1].to_string());
}
// 部署の一覧を取得
fn get_dept_list(list: &HashMap<String, Vec<String>>) -> Keys<String, Vec<String>> {
list.keys()
}
// 指定した部署の従業員の名前の一覧を取得
fn get_employee_from_dept(list: &HashMap<String, Vec<String>>,
dept: &String) -> Option<Vec<String>> {
list.get(dept).cloned()
}
// 全従業員の一覧を部署毎にソートして取得
fn get_all_employees(list: &HashMap<String, Vec<String>>)
-> Option<Vec<String>> {
// 従業員リストが空だったらNoneを返す
if list.len() == 0 {
return None;
}
let mut all_employees: Vec<String> = Vec::new();
// 部署リストを取得
let depts = get_dept_list(list);
if depts.len() == 0 {
return None; // 部署がないことはないはず
}
let mut depts :Vec<String> = depts.cloned().collect();
depts.sort();
// 部署毎に従業員リストを取得
for dept in depts {
if let Some(employees) = get_employee_from_dept(list, &dept) {
let mut employees = employees.clone();
employees.sort();
all_employees.append(&mut employees);
}
}
return Some(all_employees);
}
fn main() {
// 従業員リストへの追加データ
let add_employee_data = [
"Add Amir to Sales",
"a b c d", // error data
"Add Tom to Engineering",
"Add Sally to Engineering",
"Add Mike to Personal",
"Add Jane to Personal",
"Add Jack to Legal",
];
// 従業員リスト
let mut employee_list = HashMap::new();
// 従業員リストへの追加
for order in &add_employee_data {
add_employee(&mut employee_list, &order);
}
// 部門のリストを取得
let depts = get_dept_list(&employee_list);
let mut depts: Vec<String> = depts.cloned().collect::<Vec<String>>();
depts.sort();
// 部門毎に従業員の名前を表示
for dept in depts {
println!("member of {} dept", dept);
if let Some(employees)
= get_employee_from_dept(&employee_list, &dept) {
let mut employees = employees.clone();
employees.sort();
for employee in &employees {
println!(" {}", employee);
}
}
println!("");
}
// 全従業員リストの表示
println!("All employees");
if let Some(all_employees) = get_all_employees(&employee_list) {
for employee in all_employees {
println!(" {}", employee);
}
}
}
最初のadd_employee()でハッシュマップに部署をキーにして氏名を追加する処理。データの追加分が「Add 〜 to 〜」でなければ跳ねるようにしています。ただ、よく考えるとスペースが3個以上ない文字列を与えるとパニックしますね。不完全なプログラムになってました。
次のget_dept_list()はリストからキーになっている部署の一覧を返すだけ。そのままkeys()を呼んで結果を返してます。ここでVec<String>にでも変換して返すことも考えましたが、そのままKeys<String, Vec<String>>を返してます。
次のget_employee_from_dept()は部署をキーとして、所属従業員のベクタ(Vec<String>)を返します。これもget()を呼び出した結果を返すだけ。キーが該当しない場合もあるので、Optionになってます。受け取る方で対応が必要。
最後のget_all_employees()はすべての従業員の氏名を部署別にソートしてヴェクタにして返します。これもうまくいかない可能性があるのでOptionです。部署の順番については指定がありませんが、こちらもソートしています。
返り値をOptionではなくベクタそのものにして、処理できなかった場合には空のベクタを返すという手もあるかと思いますので、その辺は練習問題ではお好みで。実用プログラムを書くときはエラー処理をすべきと思いますが、この時点では未修なのでOptionで返してます。
余談:ヒープとスタック
ヒープとスタックという考え方は、コンピュータの基礎とかC言語とかやってないとちょっとわかりにくいかな?とは思います。そのせいか、このドキュメントでも特出しで補足説明をしています。
自分は世代的にマイクロソフトBASICからTurbo PASCAL経由でTurbo CやBorland C++なんかもやっています。また、8086特有のセグメントにもいろいろ悩まされた口。同様な経験を経てきた方ならスタックとヒープとか、自動変数とか知っていると思うので、特に問題にはならないと思います。ただ、もっと後の言語が初手だった世代には組み込み系とかやってないとあまりなじみがないのかな、と。
最近はPythonみたいな動的型付き言語が主流でそちらから入ってくる人も多いので、静的型付き言語というだけでRustの敷居が高く感じてしまう人もいそうですが、そこにあまり説明なくヒープだスタックだと専門用語が出てきても、知らない人には訳わからなそう。
とはいえ、RustはCなどと同様システムプログラミング言語でもあるので避けることはできないし、参照や借用といったRust特有の部分も絡むので避け難いのですが。所有権などと絡むから4章で触れたのでしょうが、その割には中途半端でかえって混乱を招きそう。
なぜこんなことを書いているかというと、日本語訳のスタックとヒープには訳注がついた部分があるため。こなれた訳がないとのことで一部英単語のままになってます。例えばこれ。
データを追加することは、 スタックにpushするといい、データを取り除くことは、スタックからpopすると表現します(
4.1. 所有権とは? 補足「スタックとヒープ」より訳注
: 日本語では単純に英語をそのまま活用してプッシュ、ポップと表現するでしょう)。
日本語ではスタックを「先入後出し(英語ではFILO)」とよく呼びますね。上記表現を日本語訳するなら「データを追加することはスタックに『積む』と言い、データを取り除くことはスタックから『取り出す』と表現します」ではないかと思います。
これは元々のstackの意味が「干草やお皿などが整然と積み重なったもののこと」というあたりから来ています。実際、「スタック 積む」で検索すれば普通にいろいろ出てくるのですが。イメージとしてはリボルバーじゃないオートマチック拳銃の弾倉です。上から弾丸を詰めますが、最後の弾丸が最初に発射され、最初のものが最後になります。
もちろん、訳註にある通りプッシュ、ポップと普通に言っているのでそれでも構わないと言えば構わないのですが、日本語訳するなら積む、取り出す、だよなぁと感じます。
昔はスタックの説明といえば「ハノイの塔」とか出てきたので、ますます「積む」イメージがついたのではないかとも思います。
次がメモリ管理の話。
コンパイル時にサイズがわからなかったり、サイズが可変のデータについては、代わりにヒープに格納することができます。 ヒープは、もっとごちゃごちゃしています: ヒープにデータを置く時、あるサイズのスペースを求めます。 OSはヒープ上に十分な大きさの空の領域を見つけ、使用中にし、ポインタを返します。ポインタとは、その場所へのアドレスです。 この過程は、ヒープに領域を確保する(allocating on the heap)と呼ばれ、時としてそのフレーズを単にallocateするなどと省略したりします。 (
4.1. 所有権とは? 補足「スタックとヒープ」より訳注
: こちらもこなれた日本語訳はないでしょう。allocateは「メモリを確保する」と訳したいところですが) スタックに値を積むことは、メモリ確保とは考えられません。ポインタは、既知の固定サイズなので、 スタックに保管することができますが、実データが必要になったら、ポインタを追いかける必要があります。
これは昔からメモリを「確保する」ないしは「割り当てる」と訳してきたはずなのですが。そのことを称してメモリをアロケートすると言うのは、C言語プログラマなら普通に使っていたはず(まあ、C言語プログラマの場合はその関数名からmallocすると言うことも多いですが、malloc自体がmemory allocateですから)。それなのになぜ「こなれた日本語訳はない」と断言されているのか?
非公式とは言えRust公式サイトからリンクされている日本語訳について誰もツッコミを入れないのかな?と不思議に感じています。別に知らなければならないとか言いたいわけではないですが、知識の断絶が起きているのか?とちょっと気になっています。
そういえばヒープ(heap)の本来の意味を調べたことがなかったことを思い出したので調べてみました。その結果、heapとは「積み重ね、かたまり、山」とのこと。あれ?スタックと似通ってますね。
プログラミングのヒープの意味から逆算すると、この場合は「かたまり」になるでしょうか。無理矢理な例えをすると、データが本としてヒープは本棚、スタックは手近なところにすぐ読めるように積み上げたものでしょうか。
積み上げた本はすぐに取り出せるものの、積んでるので下の方にある本を取り出すにはその上の本を一度取り出して別のところに積むなり、本棚にしまう必要があります。
一方、本棚は個別に取り出せますが、ただ、ちょっと離れたところにあります。冬の寒い日はこたつから出るのもためらいますよね。また、シリーズものや同じ分野のものをきれいに並べようとすると、本棚のスペースの都合で並べ直しが必要になることも。読み終わった本を別のところに移したら隙間が増えて、新たに入れようと思った本がうまく入らなかったり。
余談:列挙型って共用体だよね
いくつかのプログラミング言語をかじってますが、Rustの列挙型(enum)はなかなか特徴的だと思います。よくある順番付きの定数というだけでなく、特有の考え方が入ってますよね。
ただ、C/C++言語プログラマからするとこれって共用体だよね、と感じます。共用体(union)はC/C++言語の中でも割とマイナーな気がしますし、他の言語ではあまりこの考え方を見ないです。実際、自分で共用体を定義したことは多分ないはず。
Wikipediaの「共用体」の項でもC言語の他にC++とC#の例が記述されているのみ。だからと言って他の言語にこの考え方がないとは言い切れないですが、あまりメジャーではないのは想像できます。
実際のRustの列挙型の実装の詳細は知らないですが、実装面では共用体とはおそらく違うものなのだとは思います。ただ、Rustの列挙型の「1つの型の中で取りうる値を列挙する」という考え方は共用体的だなと感じます。C言語などの単なる順序付き定数とは同じ列挙型でも大違いだな、と感じます。この辺もRustの面白さであり、とっつきにくさでもあるような。
ディスカッション
コメント一覧
まだ、コメントがありません