シェルスクリプトマガジン

特集1 はじめてのRust(Vol.64記載)

著者 :河野 達也

最近、Rustというプログラミング言語の名前をよく見かけるようになりました。米Amazon Web Services社、米Dropbox社、米Facebook社、米Mozilla財団などは、Rustを使ってミッションクリティカルなソフトウエアを開発しています。Rust とはどんな言語でしょうか。シンプルなプログラムの開発を通してRustの世界に飛び込みましょう。

シェルスクリプトマガジン Vol.64は以下のリンク先でご購入できます。

Part1

図1 Rust のプログラムの例

// src/main.rsファイルの内容
//
// reqwestクレートを使うために、Cargo.tomlファイルのdependencies
// セクションに reqwest = "0.9" と書く

// main関数。プログラムが起動すると最初に呼ばれる
// この関数は引数を取らず、Result型の値を返す
fn main() -> Result<(), Box<dyn std::error::Error>> {
  // WebサービスのURI文字列をservice_uri変数にセットする
  let service_uri =
    "http://weather.livedoor.com/forecast/webservice/json/v1?city=130010";
  // 指定したURIに対してGETリクエストを送信し、レスポンスボディを取得する
  let body = reqwest::get(service_uri)?.text()?;
  // レスポンスボディを表示する
  println!("body = {:?}", body);
  // Okを返してmain関数から戻る
  // return文は不要だがこの行だけ行末にセミコロンがないことに注意
  Ok(())
}

図12 エラーになるRustのプログラムの例

// Circle型を定義する
#[derive(Debug)]
struct Circle {
  r: f64, // 円の半径、f64型
}

fn main() {
  // Circleの値をつくる
  let a = Circle{ r: 5.8 };
  // take_circle()を呼ぶとCircleの所有権が
  // 変数aから関数の引数bにムーブ
  take_circle(a);
  // aの内容を表示
  //(所有権がないのでコンパイルエラーになる)
  println!("{:?}", a);
}

fn take_circle(b: Circle) {
  // 何らかの処理

} // ここで引数bがスコープを抜けるのでCircleは削除

図21 sqrt()関数の定義コード

/// この関数はニュートン法で平方根を求めます。
fn sqrt(a: f64) -> f64 {
    // 未実装を表す。実行するとエラーになりプログラムが終了する
  unimplemented!()
}

図23 let文とif式を追加したsqrt()関数の定義コード

fn sqrt(a: f64) -> f64 {
  // 変数x0を導入し、探索の初期値に設定する
  let x0 = if a > 1.0 {
    a
  } else {
    1.0
  };
}

図24 loop式を追加したsqrt()関数の定義コード

fn sqrt(a: f64) -> f64 {
  let x0 = if a > 1.0 {
    a
  } else {
    1.0
  };
  // loopで囲まれたブロックは
  // break式が呼ばれるまで繰り返し実行される
  loop {
    // √aのニュートン法による漸化式で次項を求める
    let x1 = (x0 + a / x0) / 2.0;
    if x1 >= x0 {
      break;   // 値が減少しなくなったらloopから抜ける
    }
    x0 = x1;
  }
}

図25 戻り値の記述を追加したsqrt()関数の定義コード

fn sqrt(a: f64) -> f64 {
  let x0 = if a > 1.0 {
    a
  } else {
    1.0
  };
  loop {
    let x1 = (x0 + a / x0) / 2.0;
      if x1 >= x0 {
        break;
      }
      x0 = x1;
  }
  x0
}

図26 mut修飾子を追加したsqrt()関数の定義コード

fn sqrt(a: f64) -> f64 {
  let mut x0 = if a > 1.0 {
    a
  } else {
    1.0
  };
  loop {
    let x1 = (x0 + a / x0) / 2.0;
    if x1 >= x0 {
      break;
    }
    x0 = x1;
  }
  x0
}

図27 main()関数のコード

fn main() {
  let a = 2.0;
  // aの値とニュートン法で求めた平方根を表示
  println!("sqrt({}) = {}", a, sqrt(a));
}

Part2

図2 Cargo.tomlファイルに追加する設定

[dependencies]
chrono = "0.4"
clap = "2.33"
csv = "1.1"
hdrhistogram = "6.3"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

図3 コマンドライン引数を処理するプログラム

// clapで定義されているclap::Appとclap::Argの
// 二つの型をスコープに入れる
use clap::{App, Arg};
fn main() {
  // clap::Appでコマンド名やバージョンなどを設定
  let arg_matches = App::new("trip-analyzer")
    .version("1.0")
    .about("Analyze yellow cab trip records")
    // INFILEという名前のコマンドライン引数を登録
    .arg(Arg::with_name("INFILE")
      .help("Sets the input CSV file")
      .index(1)  // 最初の引数
    )
    // get_matches()メソッドを呼ぶとユーザーが与えた
    // コマンドライン引数がパースされる
    .get_matches();
  // INFILEの文字列を表示。"{:?}"はデバッグ用文字列を表示
  println!("INFILE: {:?}", arg_matches.value_of("INFILE"));
}

図5 Option型の定義(抜粋)

enum Option<T> {
  None,    // Noneバリアント
  Some(T), // Someバリアント。T型の値を持つ
}

図6 Result型の定義(抜粋)

Result<T, E> {
  Ok(T),   // 処理成功を示すバリアント。T型の成功時の値を持つ
  Err(E),  // 処理失敗を示すバリアント。E型のエラーを示す値を持つ
}

図7 match式とif let式の使用例

■match式の使用例

match arg_matches.value_of("INFILE") {
  // 値がパターンSome(..)にマッチするなら、
  // 包んでいる&strをinfile変数にセットし、=>以降の節を実行
  Some(infile) => println!("INFILE is {}", infile),
  // 値がパターンNoneにマッチするなら、=>以降の節を実行
  None => eprintln!("Please specify INFILE"),
}

■if let式の使用例

// 値がパターンSome(..)にマッチするなら、
// 包んでいる&strをinfile変数にセットし、true節を実行
if let Some(infile) = arg_matches.value_of("INFILE") {
  println!("INFILE is {}", infile);
} else {
  // そうでなければelse節を実行
  eprintln!("Please specify INFILE");
}

図8 コマンドライン引数を必須にする変更

(略)
    .arg(Arg::with_name("INFILE")
      .help("Sets the input CSV file")
      .index(1)
      .required(true) // この行を追加
    )
    // コマンドライン引数がない場合は
    // ここでエラーメッセージを表示してプログラム終了
    .get_matches();
  // 次の行を追加
  let infile = arg_matches.value_of("INFILE").unwrap();
  // 次の行を変更
  println!("INFILE: {}", infile);
}

図9 std::fmtモジュールのDebugトレイトの定義(抜粋)

trait Debug {
  fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error>;
}

図10 analyze()関数の定義コードを追加

use clap::{App, Arg};
// Errorトレイトをスコープに入れる
use std::error::Error;
// CSVファイルのパスを引数に取り、データを分析する
fn analyze(infile: &str) -> Result<String, Box<dyn Error>> {
  // CSVリーダーを作る。失敗したときは「?」後置演算子の働きにより、
  // analyze()関数からすぐにリターンし、処理の失敗を表すResult::Errを返す
  let mut reader = csv::Reader::from_path(infile)?;
  // 処理に成功したので(とりあえず空の文字列を包んだ)Result::Okを返す
  Ok(String::default())
}
fn main() {
(略)

図11 main()関数の定義コードを変更

fn main() {
(略)
  let infile = arg_matches.value_of("INFILE").unwrap();
  match analyze(infile) {
    Ok(json) => println!("{}", json),
    Err(e) => {
      eprintln!("Error: {}", e);
      std::process::exit(1);
    }
  }
}

図12 analyze()関数の定義コードを変更

fn analyze(infile: &str) -> Result<String, Box<dyn Error>> {
  let mut reader = csv::Reader::from_path(infile)?;
  // レコード数をカウントする
  let mut rec_counts = 0;
  // records()メソッドでCSVのレコードを一つずつ取り出す
  for result in reader.records() {
    // resultはResult<StringRecord, Error>型なので?演算子で
    // StringRecordを取り出す
    let trip = result?;
    rec_counts += 1;
    // 最初の10行だけ表示する
    if rec_counts <= 10 {
      println!("{:?}", trip);
    }
  }
  // 読み込んだレコード数を表示する
  println!("Total {} records read.", rec_counts);
  Ok(String::default())
}

図15 構造体Tripの定義コードを追加

type LocId = u16;
#[derive(Debug)]  // Debugトレイトの実装を自動導出する
struct Trip {
  pickup_datetime: String,  // 乗車日時
  dropoff_datetime: String, // 降車日時
  pickup_loc: LocId,        // 乗車地ID
  dropoff_loc: LocId,       // 降車地ID
}

図16 Tripをデシリアライズするための書き換え(その1)

// SerdeのDeserializeトレイトをスコープに入れる
use serde::Deserialize;
type LocId = u16;
// serde::Deserializeを自動導出する
#[derive(Debug, Deserialize)]
struct Trip {
  // renameアトリビュートでフィールド名と
  // CSVのカラム名を結びつける
  #[serde(rename = "tpep_pickup_datetime")]
  pickup_datetime: String,
  #[serde(rename = "tpep_dropoff_datetime")]
  dropoff_datetime: String,
  #[serde(rename = "PULocationID")]
  pickup_loc: LocId,
  #[serde(rename = "DOLocationID")]
  dropoff_loc: LocId,
}

図17 analyze()関数の定義コードを変更

fn analyze(infile: &str) -> Result<String, Box<dyn Error>> {
(略)
  let mut rec_counts = 0;
  // records()メソッドをdeserialize()メソッドに変更する
  for result in reader.deserialize() {
    // どの型にデシリアライズするかをdeserialize()メソッドに
    // 教えるために、trip変数に型アノテーションを付ける
    let trip: Trip = result?;
    rec_counts += 1;
(略)
}

図19 RecordCounts構造体の定義を追加

use serde::{Deserialize, Serialize};  // Serializeを追加
// serde_jsonでJSON文字列を生成するためにSerializeを自動導出する
#[derive(Debug, Serialize)]
struct RecordCounts {
  read: u32,    // CSVファイルから読み込んだ総レコード数
  matched: u32, // 乗車地や降車地などの条件を満たしたレコードの数
  skipped: u32, // 条件は満たしたが異常値により除外したレコードの数
}

図20 RecordCountsのデフォルト値をつくる関数を定義

impl Default for RecordCounts {
  fn default() -> Self {
    Self {
      read: 0, // read: u32::default(), としてもよい
      matched: 0,
      skipped: 0,
    }
  }
}

図21 analyze()関数の定義部分のrec_counts変数が使われている行を変更

fn analyze(infile: &str) -> Result<String, Box<dyn Error>> {
(略)
  let mut rec_counts = RecordCounts::default();
(略)
  for result in reader.deserialize() {
(略)
    rec_counts.read += 1;
    if rec_counts.read <= 10 {
(略)
  }
  println!("{:?}", rec_counts); // フォーマット文字列を変更
(略)
}

図22 日時を変換するparse_datetime()関数の定義コード

// chronoの利用にほぼ必須となる型やトレイトを一括してスコープに入れる
use chrono::prelude::*;
// NaiveDateTimeは長いのでDTという別名を定義
// chrono::NaiveDateTimeはタイムゾーンなしの日時型
type DT = NaiveDateTime;  
// ついでにResult型の別名を定義する
type AppResult<T> = Result<T, Box<dyn Error>>;
// 日時を表す文字列をDT型に変換する
fn parse_datetime(s: &str) -> AppResult<DT> {
  DT::parse_from_str(s, "%Y-%m-%d %H:%M:%S").map_err(|e| e.into())
}

図23 分析レコードを絞り込むための関数定義コードを追加(その1)

// LocIdがミッドタウン内ならtrueを返す
fn is_in_midtown(loc: LocId) -> bool {
  // LocIdの配列を作る
  let locations = [90, 100, 161, 162, 163, 164, 186, 230, 234];
  // 配列に対してバイナリーサーチする。
  // locと同じ値があればOk(値のインデックス)が返る
  locations.binary_search(&loc).is_ok()
}
// ロケーションIDがJFK国際空港ならtrueを返す
fn is_jfk_airport(loc: LocId) -> bool {
  loc == 132
}

図24 分析レコードを絞り込むための関数定義コードを追加(その2)

// 月曜から金曜ならtrueを返す
fn is_weekday(datetime: DT) -> bool {
  // 月:1, 火:2, .. 金:5, 土:6, 日:7
  datetime.weekday().number_from_monday() <= 5
}

図25 分析レコードを絞り込むためにanalyze()関数の定義コードを変更

// 戻り値型をAppResultに変更
fn analyze(infile: &str) -> AppResult<String> {
(略)
  for result in reader.deserialize() {
(略)
    if is_jfk_airport(trip.dropoff_loc) && is_in_midtown(trip.pickup_loc) {
      let pickup = parse_datetime(&trip.pickup_datetime)?;
      if is_weekday(pickup) {
        rec_counts.matched += 1;
      }
    }
  }
(略)
}

図27 統計的な処理をするためのコード

use hdrhistogram::Histogram;
// DurationHistogramsをタプル構造体として定義する
// この構造体はHistogramを24個持つことで、1時間刻みの時間帯ごとに
// 所要時間のヒストグラムデータを追跡する。
// Vec<T>型は配列の一種
struct DurationHistograms(Vec<Histogram<u64>>);
// 関連関数やメソッドを実装するためにimplブロックを作る
impl DurationHistograms {
  // Histogramsを初期化する関連関数。記録する上限値を引数に取る
  fn new() -> AppResult<Self> {
    let lower_bound = 1; // 記録する下限値。1秒
    let upper_bound = 3 * 60 * 60; // 記録する上限値。3時間
    let hist = Histogram::new_with_bounds(lower_bound, upper_bound, 3)
      .map_err(|e| format!("{:?}", e))?;
    // histの値を24回複製してVec<T>配列に収集する
    let histograms = std::iter::repeat(hist).take(24).collect();
    Ok(Self(histograms))
  }
}

図28 所要時間を登録するためのメソッドを追加(その1)

impl DurationHistograms {
  fn new() -> AppResult<Self> {
(略)
  }
  fn record_duration(&mut self, pickup: DT, dropoff: DT) -> AppResult<()> {
    // 所要時間を秒で求める。結果はi64型になるがas u64でu64型に変換
    let duration = (dropoff - pickup).num_seconds() as u64;
(略)

図29 所要時間を登録するためのメソッドを追加(その2)

impl DurationHistograms {
(略)
    let duration = (dropoff - pickup).num_seconds() as u64;
    // 20分未満はエラーにする
    if duration < 20 * 60 {
      Err(format!("duration secs {} is too short.", duration).into())
    } else {
      let hour = pickup.hour() as usize;
      // タプル構造体の最初のフィールドの名前は0になるので、
      // self.0でVec<Histogram>にアクセスできる。さらに個々の
      // Histogramにアクセスするには [インデックス] で
      // その要素のインデックスを指定する
      self.0[hour]
        // Histogramのrecord()メソッドで所要時間を記録する
        .record(duration)
        // このメソッドはHistogramの作成時に設定した上限(upper_bound)
        // を超えているとErr(RecordError)を返すので、map_err()で
        // Err(String)に変換する
        .map_err(|e| {
          format!("duration secs {} is too long. {:?}", duration, e).into()
        })
    }
  }
}

図30 統計処理をするためにanalyze()関数の定義コードを変更

fn analyze(infile: &str) -> AppResult<String> {
(略)
  let mut rec_counts = RecordCounts::default();
  let mut hist = DurationHistograms::new()?;
  // for式を変更
  for (i, result) in reader.deserialize().enumerate() {
(略)
    if is_jfk_airport((略)) {
(略)
      if is_weekday(pickup) {
        rec_counts.matched += 1;
        let dropoff = parse_datetime(&trip.dropoff_datetime)?;
        hist.record_duration(pickup, dropoff)
          .unwrap_or_else(|e| {
            eprintln!("WARN: {} - {}. Skipped: {:?}", i + 2, e, trip);
            rec_counts.skipped += 1;
          });
      }
    }       
(略)
  }
(略)
}

図32 DisplayStats構造体の定義コード

#[derive(Serialize)]
struct DisplayStats {
  record_counts: RecordCounts,
  stats: Vec<StatsEntry>,
}

図33 DisplayStats構造体の定義コード

#[derive(Serialize)]
struct StatsEntry {
  hour_of_day: u8, // 0から23。時(hour)を表す
  minimum: f64,    // 最短の所要時間
  median: f64,     // 所要時間の中央値
  #[serde(rename = "95th percentile")]
  p95: f64,        // 所要時間の95パーセンタイル値
}

図34 DisplayStats型にnew()関連関数を定義するコード

impl DisplayStats {
  fn new(record_counts: RecordCounts, histograms: DurationHistograms) -> Self {
    let stats = histograms.0.iter().enumerate()
      // mapメソッドでhdrhistogram::Histogram値からStatsEntry値を作る
      .map(|(i, hist)| StatsEntry {
        hour_of_day: i as u8,
        minimum: hist.min() as f64 / 60.0,
        median: hist.value_at_quantile(0.5) as f64 / 60.0,
        p95: hist.value_at_quantile(0.95) as f64 / 60.0,
      })
      .collect();
      Self {
        record_counts,
        stats,
      }
  }
}

図35 analyze()関数の定義コードを書き換える

let display_stats = DisplayStats::new(rec_counts, hist);
let json = serde_json::to_string_pretty(&display_stats)?;
Ok(json)