Rust初心者です。
二次元配列のデータを扱いたかったのと、JSONファイルの設定ファイルを読み込んでみたかったので調べてみました。
ついでに二項演算子のオーバーロードができるらしいので実装してみました。
コンパイルが通るところまでできたのでメモしておきます。
お題
設定ファイルで定義した二次元配列を読みだして処理したい。
設定ファイルはこんな感じ↓の JSON で。ここでは3行×3列の配列 "A" と "B" を定義している。
conf.json
{
"arrays":{
"A":{
"row_size":3,
"col_size":3,
"dat": [
11, 22, 33,
44, 55, 66,
77, 88, 99
]
},
"B":{
"row_size":3,
"col_size":3,
"dat": [
100, 200, 300,
400, 500, 600,
700, 800, 900
]
}
}
}
これを設定ファイルとして読み出す。
名前 "A" "B" で配列オブジェクトにアクセスし、配列の和を計算する。
ここで配列の和とは各要素を足し合わせた配列を生成することとする。
こんな感じで書きたい。
// 設定ファイルで定義した二次元配列 "A" の取得
let a = conf.get_array("A")?;
println!("A:\n{}", a.to_string());
// 設定ファイルで定義した二次元配列 "B" の取得
let b = conf.get_array("B")?;
println!("B:\n{}", b.to_string());
// "A" と "B" の和を計算
let c = (a + b)?;
println!("A+B:\n{}", c.to_string());
ソースコード
JSON ライブラリには serde を使用。
Cargo.toml で指定する。
[package]
name = "study0201"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.1"
// lib.rs
pub mod array;
pub mod conf;
pub mod error;
// array.rs
use std::ops;
use serde::Deserialize;
use crate::error::ErrorCode;
// 整数を要素に持つ二次元配列
#[derive(Deserialize, Debug)]
pub struct TwoDimentionalArray {
row_size:usize,
col_size:usize,
dat:Vec<i32>,
}
impl TwoDimentionalArray {
pub fn new(row_size:usize, col_size:usize, dat:Vec<i32>) -> Result<Self, ErrorCode> {
if row_size*col_size != dat.len() {
return Err(ErrorCode::RuntimeError(format!("size mismatch row_size:{} col_size:{} dat.len:{}",
row_size, col_size, dat.len())))
}
Ok(TwoDimentionalArray {
row_size: row_size,
col_size: col_size,
dat: dat,
})
}
// at(i,j) は i 行 j 列の要素を取り出す
pub fn at(&self, i:usize, j:usize) -> Option<i32> {
if i>=self.row_size || j>=self.col_size {
return None
}
Some(self.dat[i*self.row_size + j])
}
pub fn row_size(&self) -> usize {
self.row_size
}
pub fn col_size(&self) -> usize {
self.col_size
}
pub fn size(&self) -> usize {
self.row_size * self.col_size
}
// 適切なサイズかチェックする。主に JSON から deserialize した後に呼ばれる。
pub fn validate(&self) -> Result<(), ErrorCode> {
if self.row_size * self.col_size != self.dat.len() {
return Err(ErrorCode::RuntimeError(format!("size mismatch row_size:{} col_size:{} dat.len:{}",
self.row_size, self.col_size, self.dat.len())))
}
Ok(())
}
pub fn to_string(&self) -> String {
let mut r = String::from("[ ");
for (i, x) in self.dat.iter().enumerate() {
r = r + &format!("{:?} ", x);
if i == self.col_size * self.row_size - 1 {
r = r + "]"
} else if (i+1)%self.col_size == 0 {
r = r + "\n ";
}
}
r
}
}
// +演算子のオーバーロード
impl ops::Add for &TwoDimentionalArray {
type Output = Result<TwoDimentionalArray, ErrorCode>;
fn add(self, rhs: Self) -> Self::Output {
if self.row_size != rhs.row_size || self.col_size != rhs.col_size {
return Err(ErrorCode::RuntimeError(format!("add op amang mismatch arrays")))
}
let mut result:Vec<i32> = vec![];
for (a, b) in self.dat.iter().zip(rhs.dat.iter()) {
result.push(a+b)
}
Ok(TwoDimentionalArray{
row_size: self.row_size,
col_size: self.col_size,
dat:result})
}
}
// conf.rs
use std::env;
use std::fs::read_to_string;
use std::collections::HashMap;
use std::path::Path;
use serde_json;
use serde::Deserialize;
use crate::array::TwoDimentionalArray;
use crate::error::ErrorCode;
#[derive(Deserialize, Debug)]
pub struct Conf {
arrays: HashMap<String, TwoDimentionalArray>,
}
impl Conf {
pub fn new() -> Result<Self, ErrorCode> {
// 引数をチェック
let args:Vec<String> = env::args().collect();
if args.len() < 2 {
return Err(ErrorCode::ArgumentError)
}
let json_file = &args[1];
// JSONファイルの有無をチェック。ないときはエラー
validate_file_path(json_file)?;
// JSONファイルを読み込み。失敗時はIOエラー
let content = match read_to_string(json_file) {
Ok(c) => c,
Err(e) => return Err(ErrorCode::IOError(format!("{:?}", e)))
};
// JSON文字列をパース。失敗時はランタイムエラー
let deserialized_conf: Conf = match serde_json::from_str(&content) {
Ok(dc) => dc,
Err(e) => return Err(ErrorCode::RuntimeError(format!("{:?}", e)))
};
// 配列データのチェック。失敗時はエラー
deserialized_conf.validate()?;
Ok(deserialized_conf)
}
fn validate(&self) -> Result<(), ErrorCode> {
for (_, array) in &self.arrays {
array.validate()?;
}
Ok(())
}
pub fn get_array(&self, name:&str) -> Result<&TwoDimentionalArray, ErrorCode> {
match self.arrays.get(name) {
Some(r) => Ok(r),
None => Err(ErrorCode::RuntimeError(format!("unknown name: {}", name)))
}
}
}
fn validate_file_path(file_path: &str) -> Result<(), ErrorCode> {
let pb = Path::new(file_path);
if !pb.exists() {
return Err(ErrorCode::FileNotFoundError(file_path.to_string()))
}
Ok(())
}
// error.rs
use std::process::{Termination,ExitCode};
pub enum ErrorCode {
Success,
ArgumentError,
IOError(String),
FileNotFoundError(String),
RuntimeError(String),
}
impl ErrorCode {
pub fn to_value(&self) -> u8 {
match self {
ErrorCode::Success => 0,
ErrorCode::ArgumentError => 10,
ErrorCode::IOError(_) => 20,
ErrorCode::FileNotFoundError(_) => 21,
ErrorCode::RuntimeError(_) => 30,
}
}
pub fn to_string(&self) -> String {
match self {
ErrorCode::Success => "Success".to_string(),
ErrorCode::ArgumentError => "[Usage] study0201.exe conf.json".to_string(),
ErrorCode::IOError(msg) => format!("[IO Error] {}", msg),
ErrorCode::FileNotFoundError(file_path) => format!("[File Not Found] file:{}", file_path),
ErrorCode::RuntimeError(msg) => format!("[Runtime Error] {}", msg),
}
}
}
// ErrorCode に Termination トレイトを実装する
impl Termination for ErrorCode {
fn report(self) -> ExitCode {
ExitCode::from(self.to_value())
}
}
// main.rs
use study0201::array::TwoDimentionalArray;
use study0201::conf::Conf;
use study0201::error::ErrorCode;
fn main() -> ErrorCode {
// 設定ファイルの読み出し
let conf = match Conf::new() {
Ok(conf) => conf,
Err(e) => return console(e)
};
// 実行
match run(conf) {
Ok(_) => ErrorCode::Success,
Err(e) => return console(e)
}
}
fn run(conf: Conf) -> Result<(), ErrorCode> {
// 設定ファイルで定義した二次元配列 "A" の取得
let a = conf.get_array("A")?;
println!("A:\n{}", a.to_string());
// 設定ファイルで定義した二次元配列 "B" の取得
let b = conf.get_array("B")?;
println!("B:\n{}", b.to_string());
// "A" と "B" の和を計算
let c = (a + b)?;
println!("A+B:\n{}", c.to_string());
// 二次元配列をソースコードにハードコーディングしたい時
let d = &TwoDimentionalArray::new(3,3,
vec![
1,2,3,
4,5,6,
7,8,9])?;
println!("D:\n{}", d.to_string());
// at(i,j) で各要素をの取り出し
println!("0,0 = {}", d.at(0,0).unwrap());
println!("1,1 = {}", d.at(1,1).unwrap());
println!("2,2 = {}", d.at(2,2).unwrap());
// "B" と "D" の和を計算
let e = (b + d)?;
println!("B+D:\n{}", e.to_string());
Ok(())
}
fn console(e:ErrorCode) -> ErrorCode {
eprintln!("[ERROR] {}", e.to_string());
e
}
実行例。
C:\work\study0201>target\debug\study0201.exe
[ERROR] [Usage] study0201.exe conf.json
C:\work\study0201>echo %errorlevel%
10
C:\work\study0201>target\debug\study0201.exe conf.json
A:
[ 11 22 33
44 55 66
77 88 99 ]
B:
[ 100 200 300
400 500 600
700 800 900 ]
A+B:
[ 111 222 333
444 555 666
777 888 999 ]
D:
[ 1 2 3
4 5 6
7 8 9 ]
0,0 = 1
1,1 = 5
2,2 = 9
B+D:
[ 101 202 303
404 505 606
707 808 909 ]
C:\work\study0201>echo %errorlevel%
0
ここまでの感想。
- TwoDimentionalArray は当初 Vec の入れ子で考えていたが、行数・列数が事前にわかる条件下なら一次元ベクタの方が実装が簡単という結論に至りました。
- 一方、ジェネリック TwoDimentionalArray <T> にしたかったのですが、これは今の私にはまだ無理でした。
- +演算子のオーバーロードの中で、二つの配列の各要素の合計を出すところでイテレータトレイトの zip メソッドを使用。地味に便利。
- 演算子のオーバーロードは面白いですね。でも、効果的な使いどころはよくわからない。行列計算とか複素数計算のような代数的な処理しか思いつかない。
- serde での JSON データのパース(デシリアライズ)はほぼ悩むこともなく簡単に扱えました。ただし、データスキーマが不明な場合はどうするんだろうか?
- ファイルパスの検証のところ、validate_file_path 関数でまとめてみました。今回はファイルの存在確認だけ実装しましたが、今後のプログラムではほかにチェックする項目が出てくることを見越しています。
- プログラムの設定に関する処理として引数の処理と設定ファイルの読み込みまで Conf にまとめてみました。この形は流用しそうな予感。
- 以前作った ErrorCode はそのまま流用できました。ささやかな成功といえましょう。
- Result で ErrorCode を返す形でエラー処理を統一することで ? シンタックスシュガーの使いどころが理解できてきました。嬉しい。