Don't Repeat Yourself

Don't Repeat Yourself (DRY) is a principle of software development aimed at reducing repetition of all kinds. -- wikipedia

「ちょい使い」に便利なIOクレート ezio

Rust を使っているとどうしても思い出しながらでないと書けないものに標準入力、ファイルの読み書きがあります。というのも、Web アプリケーションを作るソフトウェアエンジニア(私)の場合日常業務でそこまで必要になる操作ではなく、ファイルの読み書きは S3 の SDK を叩くことのほうが多いですし、ターミナルからの入力はほぼ使うことはないためです。

しかし、ちょっとした CLI ツールを作るとなると話は変わってきます。これらはもちろんものによりますが、比較的プリミティブな標準入力操作やファイル読み書きの操作を求められるためです。そういった操作の際、Rust は Python ほどは楽に書けないというか、気をつけなければならないポイントがいくつかあり「手軽」とは呼べないことがあります。

こうした「手軽さ」のなさは面倒なポイントのひとつではありましたが、先日 ezio というクレートを作者の方が Twitter で紹介しているのを見て使ってみたところ、これが本当に使いやすくおすすめしたいと思いました。というわけで、ezio を軽く紹介する記事です。

ezio

Easy IO の略でしょうね。easy(/ˈiːzi/) + io→ez + io→ezioでしょうか。

github.com

docs.rs

作者いわく「エラーハンドリングや速度面はこだわっていないので、本番で使うには少し適さないかもしれない」とのことです。詳しくベンチマークを取ったわけではありませんが、もしかすると本番のアプリケーションのユースケースで使用が適さない場合があることに注意が必要かもしれません。しかし、手元で軽く動かす程度の CLI ツールを作る分にはこれで十分です。

特徴的なのは簡単なインタフェースで、プレリュードを一度読み込んでおくだけで、

use ezio::prelude::*;

たとえば標準入力はたったの1行で済みます。

let _ = stdio::read_line();

それでは実際に「Rust の標準ライブラリで書いたコード」と「ezio で書き直したコード」を比較しながら、どう便利になっていくのかを見てみましょう。

英和辞書ツールを作る

今回は先日発売された『手を動かして考えればよくわかる 高効率言語 Rust 書きかた・作りかた』より題材をお借りしております。実際の書籍の内容より少し改良を加えたものを使用しています。

英単語を入力すると日本語での意味を検索できる簡易的なアプリケーションを作ってみましょう。処理としては、

  1. 標準入力操作を待ち、単語をユーザーに入力させる。
  2. 裏で辞書ファイルを読み、メモリに保持させる。
  3. 1行1行一致検索を行う。

です。

Rust の標準ライブラリで

これを Rust の標準ライブラリでやると結構大変です。もちろん本番のアプリケーションではこのくらい注意深く実装する必要はあります。

標準入力は std::io::stdin().read_line() のように書くと行うことができます。普段 Rust の標準入力を使わない私は今回躓いたのですが、read_line すると末尾に改行コードがついてくるようです。そこで、改行コードを削る実装を追加しています。

ファイルの読み込みは、std::fs::Filestd::io::BufReaderstd::io::BufRead の3つの助けが必要になります。ファイルを読み込んで、それを1行1行読み込むという操作を行っています。BufReader に乗せてバッファリングし、その中身を順繰りにイテレートして一致検索を行います。

use std::fs::File;
use std::io::{stdin, BufRead, BufReader};

fn main() {
    // 1. 標準入力操作
    let mut word = String::new();
    stdin().read_line(&mut word).unwrap();
    if !word.is_empty() {
        // 末尾に改行コードがくっついてるので削る
        word.truncate(word.len() - 1);
    }

    // 2. ファイル読み込み
    let file = File::open("ejdict-hand-utf8.txt").unwrap();
    let reader = BufReader::new(file);

    // 3. 一致検索
    for line in reader.lines() {
        let line = line.unwrap();
        if line.find(&word).is_none() {
            continue;
        }
        println!("{}", line);
    }
}

同じディレクトリ内に下記のようにファイルを用意します。ファイル自体はこのサイトからダウンロードして置いています。

❯ ls --tree .
.
├── Cargo.lock
├── Cargo.toml
├── ejdict-hand-utf8.txt
├── src
...

動かすと単語の和訳の検索ができます。

❯ cargo run -q
homebrew
homebrew        自家醸造ビール

これを ezio で書き直すとどうなるでしょうか。見てみましょう。

ezio で

まず、Cargo.toml に下記を追加しましょう。

[dependencies]
ezio = "0.1.2"

ezio で書いたコードは下記のようになります。

use ezio::prelude::*;

fn main() {
    // 1. 標準入力操作
    let word = stdio::read_line();
    // 2. ファイル読み込み & 3. 一致検索
    for line in file::reader("ejdict-hand-utf8.txt") {
        if line.find(&word).is_none() {
            continue;
        }
        println!("{}", line);
    }
}

嘘みたいに短くなったことがわかります。標準入力の read_line 関数は String を返すので、わざわざ先ほどの標準ライブラリの例のように空のミュータブルな String を用意してそれを渡すといった処理は不要になります。

また、File::open とバッファリングを reader という関数で一挙に行っています。実際、reader 関数は下記のような実装になっています。

pub fn reader(path: impl AsRef<Path>) -> Reader {
    Reader(std::io::BufReader::new(
        std::fs::File::open(path).expect("Couldn't open file"),
    ))
}

このクレートは先ほども説明したとおり細かいエラーハンドリング等は行わず、内部で expect を使って Result 型は実質 unwrap されていることが多いので注意が必要ですが、簡易的な CLI ツールを作る分にはむしろこれで十分ではないかと思います。

今回は紹介しませんでしたがクレートの README に書かれているように、ファイルへの書き出しは file::write 等のシンプルな関数で済ませられます。

まとめ

  • ezio を使うと素早く標準入出力を含む操作ができる。