この記事では、v1.0.0 リリースの Go から Rust まで、私が開発してきた TUI ツールである Viddy の再実装中に得た経験と洞察を共有したいと思います。 Viddy はもともと watch コマンドの現代版として開発されましたが、今回は Rust での再実装に挑戦してみました。この記事が Rust での開発に興味がある方の参考になれば幸いです。
https://github.com/sachaos/viddy
Viddy は、Unix 系オペレーティング システムにある watch コマンドの最新の代替手段として開発されました。 watch コマンドの基本機能に加えて、Viddy は次の主要な機能を提供します。これについては、後述のデモで詳しく説明します。
当初、Viddy を Rust で実装することを目指していましたが、技術的な課題のため、より使い慣れた言語である Go を使用してリリースを優先することにしました。今回はそれらの課題を克服し、当初の目標をついに実現することができ、このリリースは私にとって特に意味のあるものになりました。
私が Go 言語自体に不満を持っていなかったことに注意することが重要です。ただし、元の実装は概念実証 (PoC) に近いものであったため、見直してみると改善したい点がたくさんありました。これらの領域は、バグの修正や機能の拡張の障害となっていました。プロジェクトをゼロから再構築したいというこの願望の高まりが、大きな動機となりました。
さらに、私は Rust に強い興味を持っており、言語の学習が進むにつれて、自分の知識を実際のプロジェクトに適用したいと思いました。 Rust については本で勉強していましたが、実際の経験がなければ、この言語のユニークな特徴を真に理解し、習得したという感覚を得るのは難しいと感じました。
再実装中の主な焦点は、リリースの優先順位付けでした。最適な実装を達成することに夢中になるのではなく、メモリ使用量やコードの簡潔さなどの最適化を延期することにし、できるだけ早くリリースすることを目指しました。このアプローチは自慢できることではないかもしれませんが、そのおかげで、私は落胆することなく、なじみのない言語での書き直しを進めることができました。
たとえば、この段階では、所有権を十分に考慮せずに、頻繁なクローン作成を使用してコードを実装しました。最適化の余地がたくさんあるため、プロジェクトには改善の余地がたくさんあります!
さらに、メソッドチェーンを使用してもっとエレガントに記述できた部分もたくさんあります。メソッド チェーンを使用すると、if ステートメントと for ステートメントの使用が減り、コードがより宣言的になった可能性があると思います。ただし、Rust の語彙が限られていることと、これ以上調査することに消極的だったこともあり、今のところ多くの部分を単純な方法で実装することにしました。
このリリースがリリースされたら、所有権を再考し、最適化を実行し、コードをリファクタリングしてこれらの懸念に対処する予定です。コードを確認して改善の余地がある点に気付いた場合は、問題を報告するか PR を送信して洞察を共有していただければ幸いです。
Rust への移行の過程で、Go と比較していくつかの長所と短所に気づきました。あくまで私の感想であり、Rust初心者なので誤解があるかも知れません。間違いや誤解を見つけた場合は、フィードバックをいただければ幸いです。
Rust では、エラーを伝播することで、エラーが発生したときに早期に復帰する簡潔なコードを書くことができます。 Go では、エラーを返すことができる関数は次のように定義されます。
func run() error { // cool code }
そして、この関数を呼び出すと、次のようにエラーを処理します。たとえば、エラーが発生した場合、呼び出し元にエラーを早めに返すことができます。
func caller() error { err := run() if err != nil { return err } fmt.Println("Success") return nil }
Rust では、エラーを返す関数は次のように記述されます。
use anyhow::Result; fn run() -> Result<()> { // cool code }
そして、呼び出し関数の早い段階でエラーを返したい場合は、? を使用して簡潔に記述することができます。演算子:
fn caller() -> Result<()> { run()?; println!("Success"); return Ok(()); }
最初はこの構文に少し戸惑いましたが、慣れると信じられないほど簡潔で便利であることがわかりました。
In Go, it's common to use pointer types to represent nullable values. However, this approach is not always safe. I often encountered runtime errors when trying to access nil elements. In Rust, the Option type allows for safe handling of nullable values. For example:
fn main() { // Define a variable of Option type let age: Option<u32> = Some(33); // Use match to handle the Option type match age { Some(value) => println!("The user's age is {}.", value), None => println!("The age is not set."), } // Use if let for concise handling if let Some(value) = age { println!("Using if let, the user's age is {}.", value); } else { println!("Using if let, the age is not set."); } // Set age to 20 if it's not defined let age = age.unwrap_or(20); }
As shown in the final example, the Option type comes with various useful methods. Using these methods allows for concise code without needing to rely heavily on if or match statements, which I find to be a significant advantage.
It's satisfying to write clean and concise code using pattern matching, method chaining, and the mechanisms mentioned earlier. It reminds me of the puzzle-like joy that programming can bring.
For example, the following function in Viddy parses a string passed as a flag to determine the command execution interval and returns a Duration.
By using the humantime crate, the function can parse time intervals specified in formats like 1s or 5m. If parsing fails, it assumes the input is in seconds and tries to parse it accordingly.
// https://github.com/sachaos/viddy/blob/4dd222edf739a672d4ca4bdd33036f524856722c/src/cli.rs#L96-L105 fn parse_duration_from_str(s: &str) -> Result<Duration> { match humantime::parse_duration(s) { Ok(d) => Ok(Duration::from_std(d)?), Err(_) => { // If the input is only a number, we assume it's in seconds let n = s.parse::<f64>()?; Ok(Duration::milliseconds((n * 1000.0) as i64)) } } }
I find it satisfying when I can use match to write code in a more declarative way. However, as I will mention later, this code can still be shortened and made even more declarative.
Thanks to features like the Option type, which ensure a certain level of safety at compile time, I found that there were fewer runtime errors during development. The fact that if the code compiles, it almost always runs without issues is something I truly appreciate.
For example, let's change the argument of the function that parses a time interval string from &str to str:
fn parse_duration_from_str(s: str /* Before: &str */) -> Result<Duration> { match humantime::parse_duration(s) { Ok(d) => Ok(Duration::from_std(d)?), Err(_) => { // If the input is only a number, we assume it's in seconds let n = s.parse::<f64>()?; Ok(Duration::milliseconds((n * 1000.0) as i64)) } } }
When you try to compile this, you get the following error:
error[E0308]: mismatched types --> src/cli.rs:97:37 | 97 | match humantime::parse_duration(s) { | ------------------------- ^ expected `&str`, found `str` | | | arguments to this function are incorrect | note: function defined here --> /Users/tsakao/.cargo/registry/src/index.crates.io-6f17d22bba15001f/humantime-2.1.0/src/duration.rs:230:8 | 230 | pub fn parse_duration(s: &str) -> Result<Duration, Error> { | ^^^^^^^^^^^^^^ help: consider borrowing here | 97 | match humantime::parse_duration(&s) { | +
As you can see from the error message, it suggests that changing the s argument in the humantime::parse_duration function to &s might fix the issue. I found the compiler’s error messages to be incredibly detailed and helpful, which is a great feature.
Now, let's move on to some aspects that I found a bit challenging.
This point is closely related to the satisfaction of writing clean code, but because Rust is so expressive and offers many ways to write code, I sometimes felt stressed thinking, "Could I write this more elegantly?" In Go, I often wrote straightforward code without overthinking it, which allowed me to focus more on the business logic rather than the specific implementation details. Personally, I saw this as a positive aspect. However, with Rust, the potential to write cleaner code often led me to spend more mental energy searching for better ways to express the logic.
For example, when I asked GitHub Copilot about the parse_duration_from_str function mentioned earlier, it suggested that it could be shortened like this:
fn parse_duration_from_str(s: &str) -> Result<Duration> { humantime::parse_duration(s) .map(Duration::from_std) .or_else(|_| s.parse::<f64>().map(|secs| Duration::milliseconds((secs * 1000.0) as i64))) }
The match expression is gone, and the code looks much cleaner—it's cool. But because Rust allows for such clean code, as a beginner still building my Rust vocabulary, I sometimes felt stressed, thinking I could probably make my code even more elegant.
Additionally, preferences for how clean or "cool" code should be can vary from person to person. I found myself a bit unsure of how far to take this approach. However, this might just be a matter of experience and the overall proficiency of the team.
As I’ll mention in a later section, I found that Rust’s standard library feels smaller compared to Go’s. In Go, the standard library is extensive and often covers most needs, making it a reliable choice. In contrast, with Rust, I often had to rely on third-party libraries.
While using third-party libraries introduces some risks, I’ve come to accept that this is just part of working with Rust.
I believe this difference may stem from the distinct use cases for Rust and Go. This is just a rough impression, but it seems that Go primarily covers web and middleware applications, while Rust spans a broader range, including web, middleware, low-level programming, systems programming, and embedded systems. Developing a standard library that covers all these areas would likely be quite costly. Additionally, since Rust’s compiler is truly outstanding, I suspect that a significant amount of development resources have been focused there.
Honestly, I do find Rust difficult at times, and I realize I need to study more. Here are some areas in Viddy that I’m using but haven’t fully grasped yet:
Additionally, since the language is so rich in features, I feel there’s a lot I don’t even know that I don’t know. As I continue to maintain Viddy, I plan to experiment and study more to deepen my understanding.
While it’s not entirely fair to compare the two languages, since the features provided aren’t exactly the same, I thought it might be interesting to compare the number of lines of source code, build times, and the number of dependencies between Rust and Go. To minimize functional differences, I measured using the RC version of Viddy (v1.0.0-rc.1), which does not include the feature that uses SQLite. For Go, I used the latest Go implementation release of Viddy (v0.4.0) for the measurements.
As I’ll mention later, the Rust implementation uses a template from the Ratatui crate, which is designed for TUI development. This template contributed to a significant amount of generated code. Additionally, some features have been added, which likely resulted in the higher line count. Generally, I found that Rust allows for more expressive code with fewer lines compared to Go.
Lines of Code | |
---|---|
Go | 1987 |
Rust | 4622 |
❯ tokei =============================================================================== Language Files Lines Code Comments Blanks =============================================================================== Go 8 1987 1579 43 365 Makefile 1 23 18 0 5 ------------------------------------------------------------------------------- (omitted) =============================================================================== Total 10 2148 1597 139 412
❯ tokei =============================================================================== Language Files Lines Code Comments Blanks =============================================================================== (omitted) ------------------------------------------------------------------------------- Rust 30 4622 4069 30 523 |- Markdown 2 81 0 48 33 (Total) 4703 4069 78 556 =============================================================================== Total 34 4827 4132 124 571 ===============================================================================
The Rust implementation includes additional features and more lines of code, so it’s not a completely fair comparison. However, even considering these factors, it’s clear that Rust builds are slower than Go builds. That said, as mentioned earlier, Rust’s compiler is extremely powerful, providing clear guidance on how to fix issues, so this slower build time is somewhat understandable.
Go | Rust | |
---|---|---|
Initial Build | 10.362s | 52.461s |
No Changes Build | 0.674s | 0.506s |
Build After Changing Code | 1.302s | 6.766s |
# After running go clean -cache ❯ time go build -ldflags="-s -w" -trimpath go build -ldflags="-s -w" -trimpath 40.23s user 11.83s system 502% cpu 10.362 total # Subsequent builds ❯ time go build -ldflags="-s -w" -trimpath go build -ldflags="-s -w" -trimpath 0.54s user 0.83s system 203% cpu 0.674 total # After modifying main.go ❯ time go build -ldflags="-s -w" -trimpath go build -ldflags="-s -w" -trimpath 1.07s user 0.95s system 155% cpu 1.302 total
# After running cargo clean ❯ time cargo build --release ...(omitted) Finished `release` profile [optimized] target(s) in 52.36s cargo build --release 627.85s user 45.07s system 1282% cpu 52.461 total # Subsequent builds ❯ time cargo build --release Finished `release` profile [optimized] target(s) in 0.40s cargo build --release 0.21s user 0.23s system 87% cpu 0.506 total # After modifying main.rs ❯ time cargo build --release Compiling viddy v1.0.0-rc.0 Finished `release` profile [optimized] target(s) in 6.67s cargo build --release 41.01s user 1.13s system 622% cpu 6.766 total
In Go, I tried to rely on the standard library as much as possible. However, as mentioned earlier, Rust's standard library (crates) is smaller compared to Go's, leading to greater reliance on external crates. When we look at the number of libraries Viddy directly depends on, the difference is quite noticeable:
Number of Dependencies | |
---|---|
Go | 13 |
Rust | 38 |
たとえば、Go では、JSON のシリアル化と逆シリアル化は標準ライブラリでサポートされていますが、Rust では、serde や serde_json などのサードパーティ クレートを使用する必要があります。さらに、非同期ランタイムにはさまざまなオプションがあり、それらを自分で選択して統合する必要があります。事実上の標準と考えられるライブラリもありますが、サードパーティのライブラリに大きく依存しているため、メンテナンスコストの増加が懸念されます。
そうは言っても、Rust では考え方を調整し、外部クレートにもっとオープンに依存することが賢明だと思われます。
このプロジェクトでは、Ratatui というクレートを使用して、Rust で TUI アプリケーションを構築しました。 Ratatui では非常に便利なテンプレートを提供しているので、ここで紹介したいと思います。
GUI アプリケーションと同様に、TUI アプリケーションはイベント駆動型です。たとえば、キーが押されるとイベントがトリガーされ、何らかのアクションが実行されます。 Ratatui は端末上で TUI ブロックをレンダリングする機能を提供しますが、それ自体ではイベントを処理しません。したがって、イベントを受信して処理するための独自のメカニズムを作成する必要があります。
Ratatui が提供するテンプレートには、このような構造が最初から含まれているため、すぐにアプリケーションを構築できます。さらに、テンプレートには、GitHub Actions、キー マッピング、ファイルから読み取ることでカスタマイズできるスタイル構成を使用した CI/CD セットアップが付属しています。
Rust で TUI を作成する予定がある場合は、これらのテンプレートの使用を検討することを強くお勧めします。
Viddy v1.0.0 が Rust で再実装されたバージョンであることをコミュニティに知らせるために、GitHub Issue および Reddit を通じて発表しました。幸いなことに、これによりさまざまなフィードバックやバグレポートが生まれ、一部の寄稿者は自分で問題を見つけて PR を提出しました。このコミュニティのサポートがなければ、多くのバグが残ったままバージョンをリリースしていたかもしれません。
この経験は私にオープンソース開発の楽しさを思い出させました。私のモチベーションも上がりましたし、コミュニティの協力に本当に感謝しています。
しばらくの間、Viddy ユーザーはコマンド出力の履歴を保存して後で確認できる機能をリクエストしていました。これに応えて、このリリースでは実行結果を SQLite に保存する「ルックバック」機能を実装しました。これにより、コマンドの終了後に Viddy を再起動して結果を確認できるようになります。この機能により、コマンド出力の変更履歴を他のユーザーと簡単に共有できるようになります。
ちなみに、「Viddy」という名前自体が映画にちなんだもので、今後も映画に関連したテーマをプロジェクトに取り入れていく予定です。私はこの新機能の「ルックバック」という名前が特に気に入っています。このテーマに合致しているからです。また、日本のアニメーション映画Look Backは本当に素晴らしかったです。
現在、Viddy は Gopher アイコンを使用していますが、実装言語が Rust に切り替わったため、混乱が生じる可能性があります。ただ、アイコンは素晴らしいのでこのままにしておくつもりです。 ?
「ヴィディ ウェル、ゴーファー、ヴィディ ウェル」というフレーズも、今では少し違った意味を帯びているかもしれません。
Viddy を Go から Rust に書き直すという挑戦を通じて、各言語の違いや特徴を深く探ることができました。 Rust のエラー伝播や Option 型などの機能は、より安全で簡潔なコードを書くのに非常に役立つことが判明しました。一方で、Rust の表現力は、特に可能な限りエレガントなコードを書かなければならないと感じたときに、ストレスの原因になることがありました。さらに、Rust の小規模な標準ライブラリが新たな課題として認識されました。
これらの課題にもかかわらず、リリースを優先し、機能するものを世に出すことに重点を置くことで、書き換えを進めることができました。 RC バージョンのテストと改善におけるコミュニティからのサポートも、大きな動機となりました。
今後も、Rust で Viddy の開発と保守を継続し、言語のスキルをさらに向上させる予定です。この記事がRustの導入を検討している方の参考になれば幸いです。最後に、Viddy のコードに改善点があれば、フィードバックや PR をいただければ幸いです。
https://github.com/sachaos/viddy
以上がGo to Rust から Viddy v.Migration のリリースの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。