この記事は Rust 3 Advent Calendar 2020 9日目の記事です。
動機
C# 1.0 と C# 9.0 で同じプログラムを書いてみよう という記事の C# 9.0 のコードを Rust に写経したらどうなるか?という試みです。
環境
stable-x86_64-unknown-linux-gnu
rustc 1.48.0 (7eac88abb 2020-11-16)
end と入力するまで文字列を受け取って最後に入力した文字列から表示する
Iterator
を用いることで下記のように、とてもエレガントで直感的な記述が可能となります。また、ある入力を除外したり変換したい場合にfilter
やmap
などの関数を使うことでスマートに書けますね。
use std::io;
struct ReadLine;
impl Iterator for ReadLine {
type Item = String;
fn next(&mut self) -> Option<Self::Item> {
let mut buf = String::new();
io::stdin().read_line(&mut buf).ok()?;
Some(buf.trim().into())
}
}
fn main() {
ReadLine
.take_while(|s| s.ne("end"))
// .filter(|s| s.ne("a"))
// .map(|s| format!("{}!", s))
.collect::<Vec<_>>()
.iter()
.rev()
.for_each(|s| println!("{}", s));
}
特定のメッセージを受け取ったらイベントを発行するクラス
C# 9.0 のEventHandler
やEventArgs
が何ものかよくわからなかったので、とりあえず構造体のメンバにクロージャを渡して、ある条件でそのクロージャを呼ぶという実装を考えてみました。Rust においてクロージャはFn
、FnMut
、FnOnce
トレイトによって実現されている訳ですが、それらのトレイトについてはこちらの記事や、こちらの記事を読むことで理解が深まると思います。
また、条件分岐にはmatches!
マクロを使用していますが、それについては matches! マクロの定義 と match 式のドキュメントが参考になります。
struct MessageCollector<F: Fn(String)> {
greet: Option<F>,
}
impl<F: Fn(String)> MessageCollector<F> {
fn new() -> Self {
Self { greet: None }
}
fn add_message(&self, message: &str) {
if matches!(message, "おはようございます" | "こんにちは" | "こんばんは")
{
if let Some(greet) = self.greet.as_ref() {
greet(message.into());
}
}
}
}
fn main() {
let greet = |message| {
println!("{} って言ったね!", message);
};
let mut message_collector = MessageCollector::new();
message_collector.greet = Some(greet);
message_collector.add_message("Hello");
message_collector.add_message("world");
message_collector.add_message("こんにちは");
}
非同期でインターネットからテキストのダウンロード
HTTP リクエストの定番はreqwest
クレートですね。2020 年 12 月現在reqwest
のバージョンは 0.10.9 ですが、tokio
バージョン 0.2.* にしか対応していないようです。(現在tokio
の最新バージョンは 0.3.5)
[dependencies]
tokio = { version = "0.2", features = ["full"] }
reqwest = "0.10"
use reqwest::Result;
#[tokio::main]
async fn main() -> Result<()> {
let body = reqwest::get("https://example.com").await?.text().await?;
println!("{}", body);
Ok(())
}
テキストファイルに書かれた URL のリストからサイトのタイトルタグの行を取得
非同期イテレータは Rust ではStream
というそうです。C# 9.0 の実装とはちょっと違いますが、非同期イテレーションの例ということで…。
async-std 版
非同期ランタイムにasync-std
を使用した場合の例。parallel-stream
という便利クレートのおかげで非同期イテレーションが簡単に書けます。
[dependencies]
anyhow = "1.0"
async-std = { version = "1.7", features = ["attributes"] }
parallel-stream = "2.1"
surf = "2.1"
use anyhow::Result;
use async_std::fs;
use parallel_stream::prelude::*;
use surf;
#[async_std::main]
async fn main() -> Result<()> {
let url_list = fs::read_to_string("urllist.txt")
.await?
.lines()
.map(String::from)
.collect::<Vec<_>>();
url_list
.into_par_stream()
.map(|url| async move {
let body = surf::get(&url).await.ok()?.body_string().await.ok()?;
for line in body.lines() {
if line.contains("<title>") {
return Some((url, line.to_string()));
}
}
None
})
.for_each(|result| async {
if let Some((url, title)) = result {
println!("{}", url);
println!("{}", title);
}
})
.await;
Ok(())
}
tokio 版
非同期ランタイムにtokio
を使用した場合の例。非同期イテレーションにはfutures
クレートのfutures::stream
を使います。
[dependencies]
anyhow = "1.0"
tokio = { version = "0.2", features = ["full"] }
reqwest = "0.10"
futures = "0.3"
use anyhow::Result;
use futures::{stream, StreamExt};
use tokio::fs;
#[tokio::main]
async fn main() -> Result<()> {
let url_list = fs::read_to_string("urllist.txt")
.await?
.lines()
.map(String::from)
.collect::<Vec<_>>();
stream::iter(url_list)
.map(|url| async move {
let body = reqwest::get(&url).await.ok()?.text().await.ok()?;
for line in body.lines() {
if line.contains("<title>") {
return Some((url, line.to_string()));
}
}
None
})
.for_each_concurrent(3, |result| async {
if let Some((url, title)) = result.await {
println!("{}", url);
println!("{}", title);
}
})
.await;
Ok(())
}
入力 2 つを整数かどうか判定して、さらに偶数かどうかも判定してメッセージを出しわける
これは前述のmatch
式の例ですね。つよい。
use anyhow::Result;
use std::io;
fn main() -> Result<()> {
let x1 = read_line()?.parse::<i32>().ok();
let x2 = read_line()?.parse::<i32>().ok();
let message = match (x1, x2) {
(Some(v1), Some(v2)) if is_even(v1) && is_even(v2) => {
format!("両方偶数!!入力した値は {} と {} ですね!", v1, v2)
}
(Some(v1), Some(v2)) => format!("入力した値は {} と {} ですね!", v1, v2),
(Some(_), None) | (None, Some(_)) => "おしいね…".into(),
(None, None) => "まだまだだね".into(),
};
println!("{}", message);
Ok(())
}
fn is_even(value: i32) -> bool {
value % 2 == 0
}
fn read_line() -> Result<String> {
let mut buf = String::new();
io::stdin().read_line(&mut buf)?;
Ok(buf.trim().into())
}
まとめ
C# 9.0 を写経してみたら Rust の便利な構文やクレートがあることに気付きました。